Skip to content
This repository was archived by the owner on Mar 14, 2021. It is now read-only.

Team 4 #5

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,4 @@ ENV/

# PyCharm
.idea/
.vscode/settings.json
101 changes: 101 additions & 0 deletions bot/cogs/snakes.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
# coding=utf-8
import logging
import json
from aiohttp import ClientSession
from random import choice
from time import time
from typing import Any, Dict

from discord import Embed, Color
from discord.ext.commands import AutoShardedBot, Context, command

from bot.snakegame import SnakeGame

log = logging.getLogger(__name__)


Expand All @@ -14,7 +21,15 @@ class Snakes:

def __init__(self, bot: AutoShardedBot):
self.bot = bot
self.game = SnakeGame((5, 5))
self.debug = True
# changed this to (User.id: int) in order to make it easier down the line to call. >(PC)
self.mods = [255254195505070081, 98694745760481280]
self.last_movement = time()
self.movement_command = {"left": 0, "right": 0, "up": 0, "down": 0}
self.wait_time = 2

# docstring for get_snek needs to be cleaned up >(PC)
async def get_snek(self, name: str = None) -> Dict[str, Any]:
"""
Go online and fetch information about a snake
Expand All @@ -29,6 +44,16 @@ async def get_snek(self, name: str = None) -> Dict[str, Any]:
:return: A dict containing information on a snake
"""

url = f'https://en.wikipedia.org/w/api.php?action=query&titles={name}' \
f'&prop=extracts&exlimit=1&explaintext&format=json&formatversion=2'

# account for snakes without a page somewhere. >(PC)
async with ClientSession() as session:
async with session.get(url) as response:
resp = json.loads(str(await response.read(), 'utf-8'))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you just return await response.json()?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I keep getting a TypeError when trying that, so I decided to build it using "json.load()". I still works exactly as I would like it to, without all the TypeError stuff.

"usable_data = web_data['query']['pages'][0]['extract']
TypeError: 'generator' object is not subscriptable"

I also completely rewrote it, so while this is still there, it's not returning anything until it's actually done doing what it's supposed to do. You'll see it in the next commit in a little while, or below if you're invested enough.

image

return resp

# docstring for get needs to be cleaned up. >(PC)
@command()
async def get(self, ctx: Context, name: str = None):
"""
Expand All @@ -40,8 +65,84 @@ async def get(self, ctx: Context, name: str = None):
:param ctx: Context object passed from discord.py
:param name: Optional, the name of the snake to get information for - omit for a random snake
"""
# Everything with snek_list should be cached >(PC)
# SELF.BOT.SNEK_LIST OMG OMG OMG >(PC)
# Since, on restart, the bot will forget this, it will be re-cached every time. Problem Solved in theory. >(PC)
possible_names = 'https://en.wikipedia.org/w/api.php?action=query&titles=List_of_snakes_by_common_name' \
'&prop=extracts&exlimit=1&explaintext&format=json&formatversion=2'

async with ClientSession() as session:
async with session.get(possible_names) as all_sneks:
resp = str(await all_sneks.read(), 'utf-8')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for specifying the encoding?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a problem with encoding when trying to change the read response to a string. Specification solved that.


# can we find a better way to do this? Doesn't seem too reliable, even though MW won't change their api. >(PC)
snek_list = resp[409:].lower().split('\\n')

# if name is None, choose a random snake. Need to clean up snek_list. >(PC)
if name is None:
name = choice(snek_list)

# stops the command if the snek is not on the list >(PC)
elif name.lower() not in snek_list:
await ctx.send('This is not a valid snake. Please request one that exists.\n'
'You can find a list of existing snakes here: ')
return

# accounting for the spaces in the names of some snakes. Allows for parsing of spaced names. >(PC)
if name.split(' '):
name = '%20'.join(name.split(' '))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the urllib module for this operation

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you specify which function of URLLib I should be using for this?


# leaving off here for the evening. Building the embed is easy. Pulling the information is hard. /s >(PC)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, build the embed. :P

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

# snek_dict = await self.get_snek(name)

# Any additional commands can be placed here. Be creative, but keep it to a reasonable amount!
@command()
async def play(self, ctx: Context, order):
"""
DiscordPlaysSnek

Move the snek around the field and collect food.

Valid use: `bot.play {direction}`

With 'left', 'right', 'up' and 'down' as valid directions.
"""

# Maybe one game at a given time, and maybe editing the original message instead of constantly posting
# new ones? Maybe we could also ask nicely for DMs to be allowed for this if they aren't. >(PC)
if order in self.movement_command.keys():
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be wrong, but it would appear that only 1 person can play at once, you should save the message author ID to a dict with their last time? I havn't played this game, so I may be misinterpreting it.

self.movement_command[order] += 1

# check if it's time to move the snek
if time() - self.last_movement > self.wait_time:
direction = max(self.movement_command,
key=self.movement_command.get)
percentage = 100*self.movement_command[direction]/sum(self.movement_command.values())
move_status = self.game.move(direction)

# end game
if move_status == "lost":
await ctx.send("We made the snek cry! :snake: :cry:")

# prepare snek message
snekembed = Embed(color=Color.red())
snekembed.add_field(name="Score", value=self.game.score)
snekembed.add_field(name="Winner movement",
value="{dir}: {per:.2f}%".format(dir=direction, per=percentage))

snek_head = next(emoji for emoji in ctx.guild.emojis if emoji.name == 'python')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very clever usage of next, I like that. However this creates issues if the name is changed. Might want to make this optional.


game_string = str(self.game).replace(":python:", str(snek_head))
snekembed.add_field(name="Board", value=game_string, inline=False)
if self.debug:
snekembed.add_field(
name="Debug", value="Debug - move_status: " + move_status, inline=False)

# prepare next movement
self.last_movement = time()
self.movement_command = {"left": 0,
"right": 0, "up": 0, "down": 0}
await ctx.send(embed=snekembed)


def setup(bot):
Expand Down
149 changes: 149 additions & 0 deletions bot/snakegame.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
from random import randint


class SnakeGame:
"""
Simple Snake game
"""

def __init__(self, board_dimensions):
self.board_dimensions = board_dimensions
self.restart()

def restart(self):
"""
Restores game to default state
"""

self.snake = Snake(positions=[[2, 2], [2, 1]])
self.putFood()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This applies to all functions and variables, use

self.put_food()

Instead!

self.score = 0

def __str__(self):
"""
Draw the board
"""
# create empty board
# board_string = "+" + "-" * (self.board_dimensions[1]) + "+" + "\n"
board_string = ""
for i in range(self.board_dimensions[0]):
# row_string = "|"
row_string = ""
# draw snake
for j in range(self.board_dimensions[1]):
if [i, j] == self.snake.positions[0]:
row_string += ":python:" # head
elif [i, j] in self.snake.positions:
row_string += "🐍"
elif [i, j] == self.food:
row_string += "🍕"
else:
row_string += "◻️"
# row_string += "|\n"
board_string += row_string + "\n"
# board_string += "+" + "-" * (self.board_dimensions[1]) + "+\n"

return board_string

def move(self, direction):
"""
Executes one movement.
Returns information about the movement:
"ok", "forbidden", "lost", "food".
"""

direction_dict = {
"right": (0, 1),
"left": (0, -1),
"up": (-1, 0),
"down": (1, 0)
}
move_status = self.snake.move(direction_dict[direction])

if self.isLost():
move_status = "lost"
self.restart()

if self.isEating():
self.snake.grow()
move_status = "grow"
self.putFood()
self.score += 1

return move_status

def isLost(self):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use python property decorator! So it would look like

@property
def lost(self):

Applies to other is functions too!

head = self.snake.head

if (head[0] == -1 or
head[1] == -1 or
head[0] == self.board_dimensions[0] or
head[1] == self.board_dimensions[1] or
head in self.snake.positions[1:]):
return True

return False

def putFood(self):
valid = False
while not valid:
i = randint(0, self.board_dimensions[0] - 1)
j = randint(0, self.board_dimensions[1] - 1)

if [i, j] not in self.snake.positions:
valid = True

self.food = [i, j]

def isEating(self):
if self.snake.head == self.food:
return True
return False


class Snake:
"""
Actual snake in the game.
"""

def __init__(self, positions):
self.positions = positions
self.head = positions[0]

def move(self, velocity):
"""
Executes one movement.

Returns information about the movement:
"ok", "forbidden"
"""
if not self.isPossible(velocity):
print("Movement not allowed")
return "forbidden"

# delete tail but store it, as we might want the snake to grow
self.deletedTail = self.positions[-1]
self.positions = self.positions[:-1]

# move head
self.head = [self.head[0] + velocity[0],
self.head[1] + velocity[1]]
self.positions.insert(0, self.head)

return "ok"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be much nicer, if it returned a bool, or enum instead, this leads to less mistakes when developing.


def isPossible(self, velocity):
"""
Check Snake is trying to do an 180º turn.
"""
newHead = [self.head[0] + velocity[0],
self.head[1] + velocity[1]]
if newHead == self.positions[1]:
return False
return True

def grow(self):
"""
Makes the snake grow one square.
"""
self.positions.append(self.deletedTail)