1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
|
import asyncio
import random
from itertools import product
import discord
from discord.ext import commands
from bot.bot import Bot
DECK = list(product(*[(0, 1, 2)]*4))
GAME_DURATION = 180
class DuckGame:
"""A class for a single game."""
def __init__(self,
rows: int = 4,
columns: int = 3,
minimum_solutions: int = 1,
) -> None:
"""
Take samples from the deck to generate a board.
Args:
rows (int, optional): Rows in the game board. Defaults to 4.
columns (int, optional): Columns in the game board. Defaults to 3.
minimum_solutions (int, optional): Minimum acceptable number of solutions in the board. Defaults to 1.
"""
self._solutions = None
size = rows * columns
self.board = random.sample(DECK, size)
while len(self.solutions) < minimum_solutions:
self.board = random.sample(DECK, size)
@property
def board(self) -> list[tuple[int]]:
"""Accesses board property."""
return self._board
@board.setter
def board(self, val: list[tuple[int]]) -> None:
"""Erases calculated solutions if the board changes."""
self._solution = None
self._board = val
@property
def solutions(self) -> None:
"""Calculate valid solutions and cache to avoid redoing work."""
if self._solutions is None:
self._solutions = set()
for idx_a, card_a in enumerate(self.board):
for idx_b, card_b in enumerate(self.board[idx_a+1:], start=idx_a+1):
"""
Two points determine a line, and there are exactly 3 points per line in {0,1,2}^4.
The completion of a line will only be a duplicate point if the other two points are the same,
which is prevented by the triangle iteration.
"""
completion = tuple(feat_a if feat_a == feat_b else 3-feat_a-feat_b
for feat_a, feat_b in zip(card_a, card_b)
)
try:
idx_c = self.board.index(completion)
except ValueError:
continue
# Indices within the solution are sorted to detect duplicate solutions modulo order.
solution = tuple(sorted((idx_a, idx_b, idx_c)))
self._solutions.add(solution)
return self._solutions
class DuckGamesDirector(commands.Cog):
"""A cog for running Duck Duck Duck Goose games."""
def __init__(self, bot: Bot) -> None:
self.bot = bot
self.current_games = {}
@commands.command(name='duckduckduckgoose', aliases=['dddg', 'duckgoose'])
@commands.cooldown(rate=1, per=2, type=commands.BucketType.channel)
async def start_game(self, ctx: commands.Context) -> None:
"""
Start a game.
The bot will post an embed with the board and will listen to the following comments for valid solutions.
Claimed answers and the final scores will be added to this embed.
"""
# One game at a time per channel
if ctx.channel.id in self.current_games:
return
game = DuckGame()
self.current_games[ctx.channel.id] = game
game.embed_msg = await self.send_board_embed(ctx, game)
await asyncio.sleep(GAME_DURATION)
"""Checking for the channel ID in the currently running games is not sufficient.
The game could have been ended by a player, and a new game already started in the same channel.
"""
if game.running:
try:
del self.current_games[ctx.channel.id]
await self.end_game(game, end_message="Time's up!")
except KeyError:
pass
async def send_board_embed(self, ctx: commands.Context, game: DuckGame) -> discord.Message:
"""Create and send the initial game embed. This will be edited as the game goes on."""
pass
def setup(bot: Bot) -> None:
"""Load the DuckGamesDirector cog."""
bot.add_cog(DuckGamesDirector(bot))
|