aboutsummaryrefslogtreecommitdiffstats
path: root/bot
diff options
context:
space:
mode:
authorGravatar vivax3794 <[email protected]>2019-08-13 02:50:32 +0200
committerGravatar vivax3794 <[email protected]>2019-08-13 02:50:32 +0200
commit1a05919ea87d8f8b64bc30dedfe7b112f2c58a94 (patch)
tree113fe74f7ed3d7bde56167c23ea9c1fa2c15eb5d /bot
parentfixed bugs and turned get_cords into a converter (diff)
change after suggestions.
Diffstat (limited to 'bot')
-rw-r--r--bot/seasons/evergreen/minesweeper.py241
1 files changed, 131 insertions, 110 deletions
diff --git a/bot/seasons/evergreen/minesweeper.py b/bot/seasons/evergreen/minesweeper.py
index 9e7fad12..60e0c2ae 100644
--- a/bot/seasons/evergreen/minesweeper.py
+++ b/bot/seasons/evergreen/minesweeper.py
@@ -1,21 +1,42 @@
+import logging
import typing
+from dataclasses import dataclass
from random import random
+import discord
from discord.ext import commands
-GameBoard = typing.List[typing.List[typing.Union[str, int]]]
-DictOfGames = typing.Dict[int, typing.Dict]
-
-
-class CordConverter(commands.Converter):
- """Converter for cords."""
-
- async def convert(self, ctx, cord: str) -> typing.Tuple[int, int]:
- """Take in a cord string and turn it into x, y"""
- if not 2 <= len(cord) <= 3:
+from bot.constants import Client
+
+MESSAGE_MAPPING = {
+ 0: ":stop_button:",
+ 1: ":one:",
+ 2: ":two:",
+ 3: ":three:",
+ 4: ":four:",
+ 5: ":five:",
+ 6: ":six:",
+ 7: ":seven:",
+ 8: ":eight:",
+ 9: ":nine:",
+ 10: ":keycap_ten:",
+ "bomb": ":bomb:",
+ "hidden": ":grey_question:",
+ "flag": ":triangular_flag_on_post:"
+}
+
+log = logging.getLogger(__name__)
+
+
+class CoordinateConverter(commands.Converter):
+ """Converter for Coordinates."""
+
+ async def convert(self, ctx, Coordinate: str) -> typing.Tuple[int, int]:
+ """Take in a Coordinate string and turn it into x, y"""
+ if not 2 <= len(Coordinate) <= 3:
raise commands.ArgumentParsingError()
- value1 = cord[0]
- value2 = cord[1:]
+ value1 = Coordinate[0]
+ value2 = Coordinate[1:]
if not value2.isdigit():
raise commands.ArgumentParsingError()
x = ord(value1) - 97
@@ -25,53 +46,53 @@ class CordConverter(commands.Converter):
return x, y
+GameDict = typing.List[typing.List[typing.Union[str, int]]]
+
+
+@dataclass
+class Game:
+ """The data for a game."""
+
+ board: GameDict
+ revealed: GameDict
+ dm_msg: discord.message
+ chat_msg: discord.message
+
+
+DictOfGames = typing.Dict[int, Game]
+
+
class Minesweeper(commands.Cog):
- """Play a game of minesweeper."""
+ """Play a game of Minesweeper."""
def __init__(self, bot: commands.Bot) -> None:
self.games: DictOfGames = {} # Store the currently running games
@staticmethod
- def is_bomb(cell: typing.Union[str, int]) -> int:
- """Returns 1 if `cell` is a bomb if not 0"""
- return cell == "bomb"
+ def get_neighbours(x: int, y: int) -> typing.Generator:
+ """Get all the neighbouring x and y including it self."""
+ for x_ in [x - 1, x, x + 1]:
+ for y_ in [y - 1, y, y + 1]:
+ if x_ != -1 and x_ != 10 and y_ != -1 and y_ != 10:
+ yield x_, y_
- def generate_board(self, bomb_chance: float) -> GameBoard:
+ def generate_board(self, bomb_chance: float) -> GameDict:
"""Generate a 2d array for the board."""
- board: GameBoard = [["bomb" if random() <= bomb_chance else "number" for _ in range(10)] for _ in range(10)]
+ board: GameDict = [["bomb" if random() <= bomb_chance else "number" for _ in range(10)] for _ in range(10)]
for y, row in enumerate(board):
for x, cell in enumerate(row):
if cell == "number":
# calculate bombs near it
bombs = 0
- for x_ in [x - 1, x, x + 1]:
- for y_ in [y - 1, y, y + 1]:
- if x_ != -1 and x_ != 10 and y_ != -1 and y_ != 10 and board[y_][x_] == "bomb":
- bombs += 1
-
+ for x_, y_ in self.get_neighbours(x, y):
+ if board[y_][x_] == "bomb":
+ bombs += 1
board[y][x] = bombs
return board
@staticmethod
- def format_for_discord(board: GameBoard) -> str:
- """Format the board to a string for discord."""
- mapping = {
- 0: ":stop_button:",
- 1: ":one:",
- 2: ":two:",
- 3: ":three:",
- 4: ":four:",
- 5: ":five:",
- 6: ":six:",
- 7: ":seven:",
- 8: ":eight:",
- 9: ":nine:",
- 10: ":keycap_ten:",
- "bomb": ":bomb:",
- "hidden": ":grey_question:",
- "flag": ":triangular_flag_on_post:"
- }
-
+ def format_for_discord(board: GameDict) -> str:
+ """Format the board as a string for Discord."""
discord_msg = (
":stop_button: :regional_indicator_a::regional_indicator_b::regional_indicator_c:"
":regional_indicator_d::regional_indicator_e::regional_indicator_f::regional_indicator_g:"
@@ -79,9 +100,9 @@ class Minesweeper(commands.Cog):
)
rows: typing.List[str] = []
for row_number, row in enumerate(board):
- new_row = mapping[row_number + 1] + " "
+ new_row = MESSAGE_MAPPING[row_number + 1] + " "
for cell in row:
- new_row += mapping[cell]
+ new_row += MESSAGE_MAPPING[cell]
rows.append(new_row)
discord_msg += "\n".join(rows)
@@ -89,124 +110,124 @@ class Minesweeper(commands.Cog):
@commands.command(name="minesweeper")
async def minesweeper_command(self, ctx: commands.Context, bomb_chance: float = .2) -> None:
- """Start a game of minesweeper."""
- if ctx.author.id in self.games.keys(): # Player is already playing
- msg = await ctx.send(f"{ctx.author.mention} you already have a game running")
+ """Start a game of Minesweeper."""
+ if ctx.author.id in self.games: # Player is already playing
+ msg = await ctx.send(f"{ctx.author.mention} you already have a game running!")
await msg.delete(delay=2)
await ctx.message.delete(delay=2)
return
# Add game to list
- board: GameBoard = self.generate_board(bomb_chance)
- revealed_board: GameBoard = [["hidden" for _ in range(10)] for _ in range(10)]
+ board: GameDict = self.generate_board(bomb_chance)
+ revealed_board: GameDict = [["hidden"] * 10 for _ in range(10)]
- await ctx.send(f"{ctx.author.mention} is playing minesweeper")
+ await ctx.send(f"{ctx.author.mention} is playing Minesweeper")
chat_msg = await ctx.send(self.format_for_discord(revealed_board))
- await ctx.author.send("play by typing: `.reveal xy [xy]` or `.flag xy [xy]` \n"
- "close the game with `.end`\n"
- "cords must be in format `<letter><number>`")
+ await ctx.author.send(
+ f"Play by typing: `{Client.prefix}reveal xy [xy]` or `{Client.prefix}flag xy [xy]` \n"
+ "Close the game with `.end`\n"
+ "Coordinates must be in format `<letter><number>`"
+ )
dm_msg = await ctx.author.send(self.format_for_discord(revealed_board))
- self.games[ctx.author.id] = {
- "board": board,
- "revealed": revealed_board,
- "dm_msg": dm_msg,
- "chat_msg": chat_msg
- }
+ self.games[ctx.author.id] = Game(
+ board=board,
+ revealed=revealed_board,
+ dm_msg=dm_msg,
+ chat_msg=chat_msg
+ )
- async def reload_board(self, ctx: commands.Context) -> None:
+ async def update_boards(self, ctx: commands.Context) -> None:
"""Update both playing boards."""
game = self.games[ctx.author.id]
- await game["dm_msg"].delete()
- game["dm_msg"] = await ctx.author.send(self.format_for_discord(game["revealed"]))
- await game["chat_msg"].edit(content=self.format_for_discord(game["revealed"]))
+ await game.dm_msg.delete()
+ game.dm_msg = await ctx.author.send(self.format_for_discord(game.revealed))
+ await game.chat_msg.edit(content=self.format_for_discord(game.revealed))
@commands.dm_only()
@commands.command(name="flag")
- async def flag_command(self, ctx: commands.Context, *cords: CordConverter) -> None:
+ async def flag_command(self, ctx: commands.Context, *Coordinates: CoordinateConverter) -> None:
"""Place multiple flags on the board"""
- board: GameBoard = self.games[ctx.author.id]["revealed"]
- for x, y in cords:
+ board: GameDict = self.games[ctx.author.id].revealed
+ for x, y in Coordinates:
if board[y][x] == "hidden":
board[y][x] = "flag"
- await self.reload_board(ctx)
+ await self.update_boards(ctx)
async def lost(self, ctx: commands.Context) -> None:
"""The player lost the game"""
game = self.games[ctx.author.id]
- game["revealed"] = game["board"]
- await self.reload_board(ctx)
- await ctx.author.send(":fire: You lost :fire: ")
- await game["chat_msg"].channel.send(f":fire: {ctx.author.mention} just lost minesweeper :fire:")
- del self.games[ctx.author.id]
+ game.revealed = game.board
+ await ctx.author.send(":fire: You lost! :fire: ")
+ await game.chat_msg.channel.send(f":fire: {ctx.author.mention} just lost minesweeper :fire:")
async def won(self, ctx: commands.Context) -> None:
"""The player won the game"""
game = self.games[ctx.author.id]
- game["revealed"] = game["board"]
- await self.reload_board(ctx)
+ game.revealed = game.board
await ctx.author.send(":tada: You won! :tada: ")
- await game["chat_msg"].channel.send(f":tada: {ctx.author.mention} just won minesweeper :tada:")
- del self.games[ctx.author.id]
-
- def reveal_zeros(self, revealed: GameBoard, board: GameBoard, x: int, y: int) -> None:
- """Used when a 0 is encountered to do a flood fill"""
- for x_ in [x - 1, x, x + 1]:
- for y_ in [y - 1, y, y + 1]:
- if x_ == -1 or x_ == 10 or y_ == -1 or y_ == 10 or revealed[y_][x_] != "hidden":
- continue
- revealed[y_][x_] = board[y_][x_]
- if board[y_][x_] == 0:
- self.reveal_zeros(revealed, board, x_, y_)
-
- async def check_if_won(self, ctx, revealed: GameBoard, board: GameBoard) -> bool:
+ await game.chat_msg.channel.send(f":tada: {ctx.author.mention} just won minesweeper :tada:")
+
+ def reveal_zeros(self, revealed: GameDict, board: GameDict, x: int, y: int) -> None:
+ """Recursively reveal adjacent cells when a 0 cell is encountered."""
+ for x_, y_ in self.get_neighbours(x, y):
+ if revealed[y_][x_] != "hidden":
+ continue
+ revealed[y_][x_] = board[y_][x_]
+ if board[y_][x_] == 0:
+ self.reveal_zeros(revealed, board, x_, y_)
+
+ async def check_if_won(self, ctx, revealed: GameDict, board: GameDict) -> bool:
"""Checks if a player has won"""
- for x_ in range(10):
- for y_ in range(10):
- if revealed[y_][x_] == "hidden" and board[y_][x_] != "bomb":
- return True
+ for x in range(10):
+ for y in range(10):
+ if revealed[y][x] == "hidden" and board[y][x] != "bomb":
+ return False
else:
await self.won(ctx)
- return False
+ return True
- async def reveal_one(self, ctx: commands.Context, revealed: GameBoard, board: GameBoard, x: int, y: int) -> bool:
+ async def reveal_one(self, ctx: commands.Context, revealed: GameDict, board: GameDict, x: int, y: int) -> bool:
"""Reveal one square."""
revealed[y][x] = board[y][x]
if board[y][x] == "bomb":
await self.lost(ctx)
- return False
+ return True # game ended
elif board[y][x] == 0:
self.reveal_zeros(revealed, board, x, y)
- return await self.check_if_won(ctx, revealed, board)
+ won = await self.check_if_won(ctx, revealed, board)
+ return won
@commands.dm_only()
@commands.command(name="reveal")
- async def reveal_command(self, ctx: commands.Context, *cords: CordConverter) -> None:
+ async def reveal_command(self, ctx: commands.Context, *Coordinates: CoordinateConverter) -> None:
"""Reveal multiple cells"""
game = self.games[ctx.author.id]
- revealed: GameBoard = game["revealed"]
- board: GameBoard = game["board"]
-
- reload_board = True
- for x, y in cords:
- if not await self.reveal_one(ctx, revealed, board, x, y):
- reload_board = False
- if reload_board:
- await self.reload_board(ctx)
+ revealed: GameDict = game.revealed
+ board: GameDict = game.board
+
+ for x, y in Coordinates:
+ if await self.reveal_one(ctx, revealed, board, x, y): # game ended
+ await self.update_boards(ctx)
+ del self.games[ctx.author.id]
+ break
+ else:
+ await self.update_boards(ctx)
@commands.command(name="end")
async def end_command(self, ctx: commands.Context):
"""End the current game"""
game = self.games[ctx.author.id]
- game["revealed"] = game["board"]
- await self.reload_board(ctx)
+ game.revealed = game.board
+ await self.update_boards(ctx)
await ctx.author.send(":no_entry: you canceled the game :no_entry:")
- await game["chat_msg"].channel.send(f"{ctx.author.mention} just canceled minesweeper")
+ await game.chat_msg.channel.send(f"{ctx.author.mention} just canceled minesweeper")
del self.games[ctx.author.id]
def setup(bot: commands.Bot) -> None:
- """Cog load."""
+ """Load the Minesweeper cog."""
bot.add_cog(Minesweeper(bot))
+ log.info("minesweeper cog loaded")