easterobot 1.3.1__py3-none-any.whl → 1.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- easterobot/bot.py +14 -1
- easterobot/casino/__init__.py +1 -0
- easterobot/casino/roulette.py +269 -0
- easterobot/commands/__init__.py +2 -0
- easterobot/commands/game.py +127 -118
- easterobot/commands/info.py +2 -2
- easterobot/commands/reset.py +11 -14
- easterobot/commands/roulette.py +34 -0
- easterobot/commands/top.py +73 -65
- easterobot/config.py +35 -8
- easterobot/games/{connect.py → connect4.py} +25 -28
- easterobot/games/game.py +126 -54
- easterobot/games/rock_paper_scissor.py +33 -30
- easterobot/games/skyjo.py +805 -0
- easterobot/games/tic_tac_toe.py +19 -18
- easterobot/hunts/hunt.py +49 -18
- easterobot/hunts/luck.py +15 -10
- easterobot/hunts/rank.py +24 -2
- easterobot/locker.py +180 -0
- easterobot/models.py +9 -0
- easterobot/resources/config.example.yml +18 -12
- easterobot/resources/credits.txt +2 -0
- easterobot/resources/emotes/placements/s1.png +0 -0
- easterobot/resources/emotes/placements/s10.png +0 -0
- easterobot/resources/emotes/placements/s11.png +0 -0
- easterobot/resources/emotes/placements/s12.png +0 -0
- easterobot/resources/emotes/placements/s2.png +0 -0
- easterobot/resources/emotes/placements/s3.png +0 -0
- easterobot/resources/emotes/placements/s4.png +0 -0
- easterobot/resources/emotes/placements/s5.png +0 -0
- easterobot/resources/emotes/placements/s6.png +0 -0
- easterobot/resources/emotes/placements/s7.png +0 -0
- easterobot/resources/emotes/placements/s8.png +0 -0
- easterobot/resources/emotes/placements/s9.png +0 -0
- easterobot/resources/emotes/placements/sA.png +0 -0
- easterobot/resources/emotes/placements/sB.png +0 -0
- easterobot/resources/emotes/placements/sC.png +0 -0
- easterobot/resources/emotes/placements/sD.png +0 -0
- easterobot/resources/emotes/placements/sE.png +0 -0
- easterobot/resources/emotes/placements/sF.png +0 -0
- easterobot/resources/emotes/placements/sG.png +0 -0
- easterobot/resources/emotes/placements/sH.png +0 -0
- easterobot/resources/emotes/placements/sI.png +0 -0
- easterobot/resources/emotes/placements/sJ.png +0 -0
- easterobot/resources/emotes/placements/sK.png +0 -0
- easterobot/resources/emotes/placements/sL.png +0 -0
- easterobot/resources/emotes/placements/sM.png +0 -0
- easterobot/resources/emotes/placements/sN.png +0 -0
- easterobot/resources/emotes/placements/sO.png +0 -0
- easterobot/resources/emotes/placements/sP.png +0 -0
- easterobot/resources/emotes/placements/sQ.png +0 -0
- easterobot/resources/emotes/placements/sR.png +0 -0
- easterobot/resources/emotes/placements/sS.png +0 -0
- easterobot/resources/emotes/placements/sT.png +0 -0
- easterobot/resources/emotes/placements/sU.png +0 -0
- easterobot/resources/emotes/placements/sV.png +0 -0
- easterobot/resources/emotes/placements/sW.png +0 -0
- easterobot/resources/emotes/placements/sX.png +0 -0
- easterobot/resources/emotes/placements/sY.png +0 -0
- easterobot/resources/emotes/placements/sZ.png +0 -0
- easterobot/resources/emotes/placements/s_.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_back.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_m1.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_m2.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p0.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p1.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p10.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p11.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p12.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p2.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p3.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p4.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p5.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p6.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p7.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p8.png +0 -0
- easterobot/resources/emotes/skyjo/skyjo_p9.png +0 -0
- {easterobot-1.3.1.dist-info → easterobot-1.5.1.dist-info}/METADATA +1 -1
- easterobot-1.5.1.dist-info/RECORD +130 -0
- easterobot-1.3.1.dist-info/RECORD +0 -70
- {easterobot-1.3.1.dist-info → easterobot-1.5.1.dist-info}/WHEEL +0 -0
- {easterobot-1.3.1.dist-info → easterobot-1.5.1.dist-info}/entry_points.txt +0 -0
- {easterobot-1.3.1.dist-info → easterobot-1.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,805 @@
|
|
1
|
+
"""Skyjo."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from enum import Enum, auto
|
6
|
+
from functools import partial
|
7
|
+
from string import ascii_uppercase
|
8
|
+
from typing import Any, Callable, Optional
|
9
|
+
|
10
|
+
import discord
|
11
|
+
from typing_extensions import override
|
12
|
+
|
13
|
+
from easterobot.bot import Easterobot
|
14
|
+
from easterobot.commands.base import Interaction
|
15
|
+
from easterobot.config import RAND
|
16
|
+
from easterobot.games.game import Button, Game, Player
|
17
|
+
|
18
|
+
CARDS = {-2: 5, -1: 10, 0: 15, **{i: 10 for i in range(1, 13)}}
|
19
|
+
logger = logging.getLogger(__name__)
|
20
|
+
COLORS = [
|
21
|
+
0xFF595E,
|
22
|
+
0x52A675,
|
23
|
+
0xFF924C,
|
24
|
+
0x1982C4,
|
25
|
+
0xFFCA3A,
|
26
|
+
0x4267AC,
|
27
|
+
0x8AC926,
|
28
|
+
0x6A4C93,
|
29
|
+
]
|
30
|
+
|
31
|
+
|
32
|
+
class ActionView(discord.ui.View):
|
33
|
+
def __init__(
|
34
|
+
self,
|
35
|
+
skyjo: "Skyjo",
|
36
|
+
) -> None:
|
37
|
+
"""ActionView."""
|
38
|
+
super().__init__(timeout=None)
|
39
|
+
self.skyjo = skyjo
|
40
|
+
if self.skyjo.starting:
|
41
|
+
self.clear_items()
|
42
|
+
options = self.skyjo.current_grid.place_options
|
43
|
+
for opt in options:
|
44
|
+
opt.emoji = self.skyjo.back
|
45
|
+
self.return_select.options = options # type: ignore[attr-defined]
|
46
|
+
self.add_item(self.return_select)
|
47
|
+
else:
|
48
|
+
self.draw_button.emoji = self.skyjo.back
|
49
|
+
self.place_select.options = self.skyjo.current_grid.place_options # type: ignore[attr-defined]
|
50
|
+
if not self.place_select.options: # type: ignore[attr-defined]
|
51
|
+
self.remove_item(self.place_select)
|
52
|
+
self.remove_item(self.return_select)
|
53
|
+
if self.skyjo.turn_state != TurnState.START:
|
54
|
+
self._update_buttons()
|
55
|
+
|
56
|
+
async def check_player(self, interaction: Interaction) -> bool:
|
57
|
+
"""Respond to interaction if invalid player."""
|
58
|
+
await interaction.response.defer() # Not the player !
|
59
|
+
return interaction.user == self.skyjo.current_player.member
|
60
|
+
|
61
|
+
@discord.ui.button(
|
62
|
+
label="Piocher une nouvelle carte",
|
63
|
+
style=discord.ButtonStyle.gray,
|
64
|
+
)
|
65
|
+
async def draw_button(
|
66
|
+
self,
|
67
|
+
interaction: Interaction,
|
68
|
+
button: Button, # noqa: ARG002
|
69
|
+
) -> None:
|
70
|
+
"""Draw button."""
|
71
|
+
if await self.check_player(interaction):
|
72
|
+
self.skyjo.draw_card()
|
73
|
+
await self.update_buttons()
|
74
|
+
|
75
|
+
async def update_buttons(self) -> None:
|
76
|
+
"""Update the view."""
|
77
|
+
self._update_buttons()
|
78
|
+
await self.skyjo.update()
|
79
|
+
|
80
|
+
def _update_buttons(self) -> None:
|
81
|
+
"""Update the view."""
|
82
|
+
self.clear_items()
|
83
|
+
if self.skyjo.current_grid.return_options:
|
84
|
+
self.return_select.options = self.skyjo.current_grid.return_options # type: ignore[attr-defined]
|
85
|
+
self.add_item(self.return_select)
|
86
|
+
if self.skyjo.current_grid.place_options:
|
87
|
+
self.place_select.options = self.skyjo.current_grid.place_options # type: ignore[attr-defined]
|
88
|
+
self.add_item(self.place_select)
|
89
|
+
|
90
|
+
@discord.ui.select(placeholder="Retourner une de mes cartes", options=[])
|
91
|
+
async def return_select(
|
92
|
+
self, interaction: Interaction, select: discord.ui.Select["ActionView"]
|
93
|
+
) -> None:
|
94
|
+
"""Take button."""
|
95
|
+
place: str = select.values[0]
|
96
|
+
if self.skyjo.starting:
|
97
|
+
for p in self.skyjo.players:
|
98
|
+
if interaction.user == p.member:
|
99
|
+
grid = self.skyjo.grids[p]
|
100
|
+
if grid.ready:
|
101
|
+
logger.info(
|
102
|
+
"%s (%s) has already return his cards",
|
103
|
+
p.member.display_name,
|
104
|
+
p.member.id,
|
105
|
+
)
|
106
|
+
await interaction.response.defer()
|
107
|
+
else:
|
108
|
+
card = grid.get(place)
|
109
|
+
await interaction.response.defer()
|
110
|
+
if card.hidden:
|
111
|
+
grid.return_card(place)
|
112
|
+
logger.info(
|
113
|
+
"%s (%s) return %s with value %s",
|
114
|
+
p.member.display_name,
|
115
|
+
p.member.id,
|
116
|
+
place,
|
117
|
+
card.value,
|
118
|
+
)
|
119
|
+
await self.skyjo.update()
|
120
|
+
else:
|
121
|
+
logger.info(
|
122
|
+
"%s (%s) try to return a already visible card",
|
123
|
+
p.member.display_name,
|
124
|
+
p.member.id,
|
125
|
+
)
|
126
|
+
return
|
127
|
+
logger.info(
|
128
|
+
"%s (%s) is not part of the game",
|
129
|
+
interaction.user.display_name,
|
130
|
+
interaction.user.id,
|
131
|
+
)
|
132
|
+
await interaction.response.defer()
|
133
|
+
return
|
134
|
+
if await self.check_player(interaction):
|
135
|
+
self.skyjo.remove_timeout_penalty()
|
136
|
+
self.skyjo.return_card(place)
|
137
|
+
await self.update_selects()
|
138
|
+
return
|
139
|
+
|
140
|
+
@discord.ui.select(placeholder="Prendre et remplacer", options=[])
|
141
|
+
async def place_select(
|
142
|
+
self, interaction: Interaction, select: discord.ui.Select["ActionView"]
|
143
|
+
) -> None:
|
144
|
+
"""Take button."""
|
145
|
+
if await self.check_player(interaction):
|
146
|
+
place: str = select.values[0]
|
147
|
+
self.skyjo.remove_timeout_penalty()
|
148
|
+
self.skyjo.place_card(place)
|
149
|
+
await self.update_selects()
|
150
|
+
|
151
|
+
async def update_selects(self) -> None:
|
152
|
+
"""Update select."""
|
153
|
+
self.clear_items()
|
154
|
+
self.add_item(self.draw_button)
|
155
|
+
await self.skyjo.update()
|
156
|
+
|
157
|
+
|
158
|
+
@dataclass
|
159
|
+
class Card:
|
160
|
+
value: int
|
161
|
+
value_emoji: discord.PartialEmoji
|
162
|
+
hidden: bool
|
163
|
+
hidden_emoji: discord.PartialEmoji
|
164
|
+
|
165
|
+
def copy(self) -> "Card":
|
166
|
+
"""Get the current emoji."""
|
167
|
+
return Card(
|
168
|
+
self.value,
|
169
|
+
self.value_emoji,
|
170
|
+
self.hidden,
|
171
|
+
self.hidden_emoji,
|
172
|
+
)
|
173
|
+
|
174
|
+
@property
|
175
|
+
def emoji(self) -> discord.PartialEmoji:
|
176
|
+
"""Get the current emoji."""
|
177
|
+
if self.hidden:
|
178
|
+
return self.hidden_emoji
|
179
|
+
return self.value_emoji
|
180
|
+
|
181
|
+
def __str__(self) -> str:
|
182
|
+
"""Return the str representation of the card."""
|
183
|
+
return str(self.emoji)
|
184
|
+
|
185
|
+
|
186
|
+
class AI:
|
187
|
+
def __init__(self, skyjo: "Skyjo") -> None:
|
188
|
+
"""Instantiate IA."""
|
189
|
+
self.skyjo = skyjo
|
190
|
+
|
191
|
+
def hypothetic_value(self, grid: "Grid") -> float:
|
192
|
+
"""Hypothetic value."""
|
193
|
+
value = sum(
|
194
|
+
5.0666 if c.hidden else c.value for c in grid.cards.values()
|
195
|
+
)
|
196
|
+
# Add bonus for row with double
|
197
|
+
turn_left = self.minimal_turn_left()
|
198
|
+
ratio = 1 - max(turn_left / 10, 1.0)
|
199
|
+
ratio = 2 + ratio * 6
|
200
|
+
columns = len(grid.content[0])
|
201
|
+
for x in range(columns):
|
202
|
+
for y in range(3):
|
203
|
+
card = grid.content[y][x]
|
204
|
+
if card.hidden or card.value <= 0:
|
205
|
+
continue
|
206
|
+
for y2 in range(3):
|
207
|
+
card_cmp = grid.content[y2][x]
|
208
|
+
if y == y2 or card_cmp.hidden:
|
209
|
+
continue
|
210
|
+
if card_cmp.value == card.value:
|
211
|
+
value -= card.value / ratio
|
212
|
+
|
213
|
+
# Penalty for row with broken combo
|
214
|
+
for x in range(columns):
|
215
|
+
c1 = grid.content[0][x]
|
216
|
+
c2 = grid.content[0][x]
|
217
|
+
c3 = grid.content[0][x]
|
218
|
+
if c1.hidden or c2.hidden or c3.hidden:
|
219
|
+
continue
|
220
|
+
if (
|
221
|
+
c1.value == c2.value # noqa: PLR1714
|
222
|
+
or c1.value == c3.value
|
223
|
+
or c2.value == c3.value
|
224
|
+
):
|
225
|
+
value += 1.5
|
226
|
+
return value
|
227
|
+
|
228
|
+
async def random_starting(self) -> None:
|
229
|
+
"""Random starting."""
|
230
|
+
for grid in self.skyjo.grids.values():
|
231
|
+
while not grid.ready:
|
232
|
+
rand_place = RAND.choice(
|
233
|
+
[
|
234
|
+
place
|
235
|
+
for place, card in grid.cards.items()
|
236
|
+
if card.hidden
|
237
|
+
]
|
238
|
+
)
|
239
|
+
grid.return_card(rand_place)
|
240
|
+
|
241
|
+
# Update view
|
242
|
+
await self.skyjo.update()
|
243
|
+
|
244
|
+
def near_full(self, grid: "Grid") -> bool:
|
245
|
+
"""Return True if the grid is nearly full."""
|
246
|
+
return sum(c.hidden for c in grid.cards.values()) <= 1
|
247
|
+
|
248
|
+
async def play(self) -> None:
|
249
|
+
"""Play for player."""
|
250
|
+
# Compute all actions
|
251
|
+
actions: list[tuple[Callable[[], Any], float, bool]] = []
|
252
|
+
grid = self.skyjo.current_grid
|
253
|
+
|
254
|
+
# Replace card
|
255
|
+
if self.skyjo.turn_state == TurnState.START:
|
256
|
+
# Take the card or return
|
257
|
+
current_card = self.skyjo.current_card
|
258
|
+
initial_value = self.hypothetic_value(grid)
|
259
|
+
for i_line, line in enumerate(grid.content):
|
260
|
+
for i_col, card in enumerate(line):
|
261
|
+
place = grid.place(i_line, i_col)
|
262
|
+
grid_copy = grid.copy()
|
263
|
+
grid_copy.replace_card(place, current_card)
|
264
|
+
penalty = 0 if card.hidden else 2.5
|
265
|
+
actions.append(
|
266
|
+
(
|
267
|
+
partial(self.skyjo.place_card, place),
|
268
|
+
self.hypothetic_value(grid_copy) + penalty,
|
269
|
+
grid_copy.full,
|
270
|
+
)
|
271
|
+
)
|
272
|
+
|
273
|
+
# Compute esperance of take the new card
|
274
|
+
total_card = sum(CARDS.values())
|
275
|
+
hypothetic_value_draw = 0.0
|
276
|
+
for c, n in CARDS.items():
|
277
|
+
prob = n / total_card
|
278
|
+
values = []
|
279
|
+
card = self.skyjo.card(c, hidden=True)
|
280
|
+
for i_line, line in enumerate(grid.content):
|
281
|
+
for i_col, card in enumerate(line):
|
282
|
+
place = grid.place(i_line, i_col)
|
283
|
+
grid_copy = grid.copy()
|
284
|
+
grid_copy.replace_card(place, card)
|
285
|
+
value = self.hypothetic_value(grid_copy)
|
286
|
+
if card.hidden and value < initial_value:
|
287
|
+
value = initial_value
|
288
|
+
values.append(value)
|
289
|
+
hypothetic_value_draw += min(values) * prob
|
290
|
+
|
291
|
+
# Choose between draw and place
|
292
|
+
RAND.shuffle(actions)
|
293
|
+
action, value, end = min(
|
294
|
+
actions,
|
295
|
+
key=lambda t: (
|
296
|
+
not (t[2] and self.is_best_player(t[1])),
|
297
|
+
(t[2] and not self.is_best_player(t[1])),
|
298
|
+
t[1],
|
299
|
+
),
|
300
|
+
)
|
301
|
+
if value < hypothetic_value_draw:
|
302
|
+
action() # make the action
|
303
|
+
await self.skyjo.update()
|
304
|
+
return
|
305
|
+
|
306
|
+
# Redraw !
|
307
|
+
self.skyjo.draw_card()
|
308
|
+
|
309
|
+
# Recompute actions
|
310
|
+
current_card = self.skyjo.current_card
|
311
|
+
initial_value = self.hypothetic_value(grid)
|
312
|
+
for i_line, line in enumerate(grid.content):
|
313
|
+
for i_col, card in enumerate(line):
|
314
|
+
place = grid.place(i_line, i_col)
|
315
|
+
if card.hidden:
|
316
|
+
actions.append(
|
317
|
+
(
|
318
|
+
partial(self.skyjo.return_card, place),
|
319
|
+
initial_value,
|
320
|
+
self.near_full(grid),
|
321
|
+
)
|
322
|
+
)
|
323
|
+
grid_copy = grid.copy()
|
324
|
+
grid_copy.replace_card(place, current_card)
|
325
|
+
penalty = 0 if card.hidden else 2.5
|
326
|
+
actions.append(
|
327
|
+
(
|
328
|
+
partial(self.skyjo.place_card, place),
|
329
|
+
self.hypothetic_value(grid_copy) + penalty,
|
330
|
+
grid_copy.full,
|
331
|
+
)
|
332
|
+
)
|
333
|
+
|
334
|
+
# Pick the best action
|
335
|
+
RAND.shuffle(actions)
|
336
|
+
action, value, end = min(
|
337
|
+
actions,
|
338
|
+
key=lambda t: (
|
339
|
+
not (t[2] and self.is_best_player(t[1])),
|
340
|
+
(t[2] and not self.is_best_player(t[1])),
|
341
|
+
t[1],
|
342
|
+
),
|
343
|
+
)
|
344
|
+
action()
|
345
|
+
|
346
|
+
# Update view
|
347
|
+
await self.skyjo.update()
|
348
|
+
|
349
|
+
def is_best_player(self, score: float) -> bool:
|
350
|
+
"""Check if the score is the best player."""
|
351
|
+
max_enemy_value = max(
|
352
|
+
self.hypothetic_value(grid)
|
353
|
+
for p, grid in self.skyjo.grids.items()
|
354
|
+
if p != self.skyjo.current_player
|
355
|
+
)
|
356
|
+
return max_enemy_value < score
|
357
|
+
|
358
|
+
def minimal_turn_left(self) -> int:
|
359
|
+
"""Get minimal turn left."""
|
360
|
+
return min(
|
361
|
+
[
|
362
|
+
sum(card.hidden for line in grid.content for card in line)
|
363
|
+
for grid in self.skyjo.grids.values()
|
364
|
+
],
|
365
|
+
default=0,
|
366
|
+
)
|
367
|
+
|
368
|
+
|
369
|
+
@dataclass
|
370
|
+
class Grid:
|
371
|
+
content: list[list[Card]]
|
372
|
+
app_emojis: dict[str, discord.Emoji]
|
373
|
+
|
374
|
+
def copy(self) -> "Grid":
|
375
|
+
"""Copy the grid."""
|
376
|
+
return Grid(
|
377
|
+
[[c.copy() for c in line] for line in self.content],
|
378
|
+
app_emojis=self.app_emojis,
|
379
|
+
)
|
380
|
+
|
381
|
+
def index(self, place: str) -> tuple[int, int]:
|
382
|
+
"""Get index from place."""
|
383
|
+
letter = ascii_uppercase.index(place[0])
|
384
|
+
digit = int(place[1:]) - 1
|
385
|
+
return digit, letter
|
386
|
+
|
387
|
+
def return_card(self, place: str) -> Card:
|
388
|
+
"""Return a card."""
|
389
|
+
x, y = self.index(place)
|
390
|
+
card = self.content[x][y]
|
391
|
+
card.hidden = False
|
392
|
+
removed_card = self._remove_combo()
|
393
|
+
return removed_card if removed_card else card
|
394
|
+
|
395
|
+
def return_all_card(self) -> None:
|
396
|
+
"""Cards."""
|
397
|
+
for card in self.cards.values():
|
398
|
+
card.hidden = False
|
399
|
+
|
400
|
+
def replace_card(self, place: str, card: Card) -> Card:
|
401
|
+
"""Replace a card."""
|
402
|
+
x, y = self.index(place)
|
403
|
+
prev = self.content[x][y]
|
404
|
+
self.content[x][y] = card
|
405
|
+
prev.hidden = False
|
406
|
+
removed_card = self._remove_combo()
|
407
|
+
return removed_card if removed_card else prev
|
408
|
+
|
409
|
+
def _remove_combo(self) -> Optional[Card]:
|
410
|
+
columns = len(self.content[0])
|
411
|
+
removed_card = None
|
412
|
+
for x in range(columns - 1, -1, -1):
|
413
|
+
card = self.content[0][x]
|
414
|
+
if card.hidden:
|
415
|
+
continue
|
416
|
+
for y in range(1, 3):
|
417
|
+
other = self.content[y][x]
|
418
|
+
if other.hidden or other.value != card.value:
|
419
|
+
break
|
420
|
+
else:
|
421
|
+
# Combo !
|
422
|
+
removed_card = card
|
423
|
+
for y in range(3):
|
424
|
+
del self.content[y][x]
|
425
|
+
return removed_card
|
426
|
+
|
427
|
+
def get(self, place: str) -> Card:
|
428
|
+
"""Return a card."""
|
429
|
+
x, y = self.index(place)
|
430
|
+
return self.content[x][y]
|
431
|
+
|
432
|
+
def __str__(self) -> str:
|
433
|
+
"""Get the string representation."""
|
434
|
+
if not self.content:
|
435
|
+
return str(self.app_emojis["s_"])
|
436
|
+
return (
|
437
|
+
str(self.app_emojis["s_"])
|
438
|
+
+ "".join(
|
439
|
+
str(self.app_emojis["s" + ascii_uppercase[n]])
|
440
|
+
for n in range(len(self.content[0]))
|
441
|
+
)
|
442
|
+
+ "\n"
|
443
|
+
+ "\n".join(
|
444
|
+
str(self.app_emojis["s" + str(i)])
|
445
|
+
+ "".join(str(card) for card in line)
|
446
|
+
for i, line in enumerate(self.content, start=1)
|
447
|
+
)
|
448
|
+
+ "\n\n"
|
449
|
+
)
|
450
|
+
|
451
|
+
@property
|
452
|
+
def ready(self) -> bool:
|
453
|
+
"""Cards."""
|
454
|
+
return sum(not card.hidden for card in self.cards.values()) >= 2 # noqa: PLR2004
|
455
|
+
|
456
|
+
@property
|
457
|
+
def value(self) -> int:
|
458
|
+
"""Compute value."""
|
459
|
+
return sum(c.value for c in self.cards.values())
|
460
|
+
|
461
|
+
@property
|
462
|
+
def value_visible(self) -> int:
|
463
|
+
"""Compute value."""
|
464
|
+
return sum(c.value for c in self.cards.values() if not c.hidden)
|
465
|
+
|
466
|
+
@property
|
467
|
+
def full(self) -> bool:
|
468
|
+
"""Cards."""
|
469
|
+
return all(not card.hidden for card in self.cards.values())
|
470
|
+
|
471
|
+
@property
|
472
|
+
def cards(self) -> dict[str, Card]:
|
473
|
+
"""Cards."""
|
474
|
+
cards = {}
|
475
|
+
for digit, line in enumerate(self.content, start=1):
|
476
|
+
for x, card in enumerate(line):
|
477
|
+
letter = ascii_uppercase[x]
|
478
|
+
place = f"{letter}{digit}"
|
479
|
+
cards[place] = card
|
480
|
+
return cards
|
481
|
+
|
482
|
+
def place(self, i_line: int, i_col: int) -> str:
|
483
|
+
"""Get the place."""
|
484
|
+
return f"{ascii_uppercase[i_col]}{i_line + 1}"
|
485
|
+
|
486
|
+
@property
|
487
|
+
def place_options(self) -> list[discord.SelectOption]:
|
488
|
+
"""Place options."""
|
489
|
+
return sorted(
|
490
|
+
[
|
491
|
+
discord.SelectOption(
|
492
|
+
label=place, emoji=card.emoji, value=place
|
493
|
+
)
|
494
|
+
for place, card in self.cards.items()
|
495
|
+
],
|
496
|
+
key=lambda o: o.label,
|
497
|
+
)
|
498
|
+
|
499
|
+
@property
|
500
|
+
def return_options(self) -> list[discord.SelectOption]:
|
501
|
+
"""Return options."""
|
502
|
+
return sorted(
|
503
|
+
[
|
504
|
+
discord.SelectOption(
|
505
|
+
label=place, emoji=card.emoji, value=place
|
506
|
+
)
|
507
|
+
for place, card in self.cards.items()
|
508
|
+
if card.hidden
|
509
|
+
],
|
510
|
+
key=lambda o: o.label,
|
511
|
+
)
|
512
|
+
|
513
|
+
|
514
|
+
class TurnState(Enum):
|
515
|
+
START = auto()
|
516
|
+
DRAW = auto()
|
517
|
+
|
518
|
+
|
519
|
+
class Skyjo(Game):
|
520
|
+
emojis: dict[discord.PartialEmoji, int]
|
521
|
+
|
522
|
+
def __init__(
|
523
|
+
self,
|
524
|
+
bot: Easterobot,
|
525
|
+
message: discord.Message,
|
526
|
+
*members: discord.Member,
|
527
|
+
) -> None:
|
528
|
+
"""Instantiate Connect4."""
|
529
|
+
self.turn = 0
|
530
|
+
self.decks: list[int] = []
|
531
|
+
self.starting = True
|
532
|
+
self.finish_turn: Optional[int] = None
|
533
|
+
self.turn_state = TurnState.START
|
534
|
+
self.timeout_penalty: dict[Player, float] = {}
|
535
|
+
super().__init__(bot, message, *members)
|
536
|
+
|
537
|
+
def recreate_decks(self) -> None:
|
538
|
+
"""Recrate decks."""
|
539
|
+
self.decks = [
|
540
|
+
value for value, cards in CARDS.items() for _ in range(cards)
|
541
|
+
]
|
542
|
+
RAND.shuffle(self.decks)
|
543
|
+
|
544
|
+
@classmethod
|
545
|
+
def maximum_player(cls) -> int:
|
546
|
+
"""Get the maximum player number."""
|
547
|
+
return 8
|
548
|
+
|
549
|
+
@property
|
550
|
+
def current_player(self) -> Player:
|
551
|
+
"""Get current player."""
|
552
|
+
return self.players[self.turn % len(self.players)]
|
553
|
+
|
554
|
+
@property
|
555
|
+
def current_grid(self) -> Grid:
|
556
|
+
"""Get current player."""
|
557
|
+
return self.grids[self.current_player]
|
558
|
+
|
559
|
+
@property
|
560
|
+
def finish_player(self) -> Optional[Player]:
|
561
|
+
"""Get the final player."""
|
562
|
+
if self.finish_turn is not None:
|
563
|
+
return self.players[self.finish_turn % len(self.players)]
|
564
|
+
return None
|
565
|
+
|
566
|
+
def card_value_to_emoji_name(self, value: int) -> str:
|
567
|
+
"""Get name of card from point."""
|
568
|
+
return "skyjo_" + (f"m{-value}" if value < 0 else f"p{value}")
|
569
|
+
|
570
|
+
def draw_hidden_card(self) -> Card:
|
571
|
+
"""Draw card."""
|
572
|
+
if not self.decks:
|
573
|
+
self.recreate_decks()
|
574
|
+
card = self.decks.pop()
|
575
|
+
return self.card(card, hidden=True)
|
576
|
+
|
577
|
+
def remove_timeout_penalty(self) -> None:
|
578
|
+
"""Remove the timeout penalty."""
|
579
|
+
if self.current_player in self.timeout_penalty:
|
580
|
+
del self.timeout_penalty[self.current_player]
|
581
|
+
|
582
|
+
def draw_card(self) -> Card:
|
583
|
+
"""Draw card."""
|
584
|
+
self.current_card = self.draw_hidden_card()
|
585
|
+
self.current_card.hidden = False
|
586
|
+
logger.info(
|
587
|
+
"%s (%s) draw card of value %s",
|
588
|
+
self.current_player.member.display_name,
|
589
|
+
self.current_player.member.id,
|
590
|
+
self.current_card.value,
|
591
|
+
)
|
592
|
+
self.turn_state = TurnState.DRAW
|
593
|
+
return self.current_card
|
594
|
+
|
595
|
+
def return_card(self, place: str) -> Card:
|
596
|
+
"""Return a card."""
|
597
|
+
card = self.current_grid.return_card(place)
|
598
|
+
logger.info(
|
599
|
+
"%s (%s) return card at %s and got %s",
|
600
|
+
self.current_player.member.display_name,
|
601
|
+
self.current_player.member.id,
|
602
|
+
place,
|
603
|
+
card.value,
|
604
|
+
)
|
605
|
+
self.turn += 1
|
606
|
+
self.turn_state = TurnState.START
|
607
|
+
return card
|
608
|
+
|
609
|
+
def place_card(self, place: str) -> Card:
|
610
|
+
"""Place a card."""
|
611
|
+
card = self.current_card
|
612
|
+
self.current_card = self.current_grid.replace_card(
|
613
|
+
place,
|
614
|
+
self.current_card,
|
615
|
+
)
|
616
|
+
logger.info(
|
617
|
+
"%s (%s) replace his card at %s of value %s by %s",
|
618
|
+
self.current_player.member.display_name,
|
619
|
+
self.current_player.member.id,
|
620
|
+
place,
|
621
|
+
self.current_card.value,
|
622
|
+
card.value,
|
623
|
+
)
|
624
|
+
self.turn += 1
|
625
|
+
self.turn_state = TurnState.START
|
626
|
+
return self.current_card
|
627
|
+
|
628
|
+
def card(self, value: int, *, hidden: bool) -> Card:
|
629
|
+
"""Get the card."""
|
630
|
+
return Card(
|
631
|
+
value=value,
|
632
|
+
value_emoji=self.cards[value],
|
633
|
+
hidden=hidden,
|
634
|
+
hidden_emoji=self.back,
|
635
|
+
)
|
636
|
+
|
637
|
+
async def on_start(self) -> None:
|
638
|
+
"""Run."""
|
639
|
+
self.cards = {
|
640
|
+
i: self.bot.app_emojis[ # noqa: SLF001
|
641
|
+
self.card_value_to_emoji_name(i)
|
642
|
+
]._to_partial()
|
643
|
+
for i in range(-2, 13, 1)
|
644
|
+
}
|
645
|
+
self.back = self.bot.app_emojis["skyjo_back"]._to_partial() # noqa: SLF001
|
646
|
+
self.grids = {
|
647
|
+
p: Grid(
|
648
|
+
[
|
649
|
+
[self.draw_hidden_card() for x in range(4)]
|
650
|
+
for line in range(3)
|
651
|
+
],
|
652
|
+
app_emojis=self.bot.app_emojis,
|
653
|
+
)
|
654
|
+
for p in self.players
|
655
|
+
}
|
656
|
+
self.current_card = self.draw_hidden_card()
|
657
|
+
self.current_card.hidden = False
|
658
|
+
await self.start_timer(120)
|
659
|
+
await self.update()
|
660
|
+
|
661
|
+
async def update(self) -> None: # noqa: C901, PLR0912, PLR0915
|
662
|
+
"""Update the text."""
|
663
|
+
# Instantiate view and embed
|
664
|
+
embed = discord.Embed()
|
665
|
+
|
666
|
+
# End of starting phase
|
667
|
+
if self.starting and all(grid.ready for grid in self.grids.values()):
|
668
|
+
logger.info("All players have 2 card face up")
|
669
|
+
self.starting = False
|
670
|
+
self.players.sort(
|
671
|
+
key=lambda p: self.grids[p].value_visible,
|
672
|
+
reverse=True,
|
673
|
+
)
|
674
|
+
|
675
|
+
# One player have complete one of his grid
|
676
|
+
if (
|
677
|
+
any(grid.full for grid in self.grids.values())
|
678
|
+
and self.finish_turn is None
|
679
|
+
):
|
680
|
+
self.finish_turn = self.turn + len(self.players) - 1
|
681
|
+
logger.info("Last turn will be %s", self.finish_turn)
|
682
|
+
assert self.finish_player # noqa: S101
|
683
|
+
logger.info(
|
684
|
+
"Finisher is %s (%s)",
|
685
|
+
self.finish_player.member.display_name,
|
686
|
+
self.finish_player.member.id,
|
687
|
+
)
|
688
|
+
|
689
|
+
# The game is finished
|
690
|
+
scores = None
|
691
|
+
header = "Partie en cours"
|
692
|
+
if self.finish_turn is not None and self.finish_turn == self.turn:
|
693
|
+
await self.stop_timer()
|
694
|
+
logger.info("Game is finished !")
|
695
|
+
scores = self.scores()
|
696
|
+
header = "Partie terminée"
|
697
|
+
best_score = float("+inf")
|
698
|
+
best_players = []
|
699
|
+
for p, s in scores.items():
|
700
|
+
if s < best_score:
|
701
|
+
best_score = s
|
702
|
+
best_players = [p]
|
703
|
+
elif s == best_score:
|
704
|
+
best_players.append(p)
|
705
|
+
if len(best_players) >= 2: # noqa: PLR2004
|
706
|
+
content = "## Égalité entre "
|
707
|
+
content += ", ".join(
|
708
|
+
p.member.mention for p in best_players[:-1]
|
709
|
+
)
|
710
|
+
content += f" et {best_players[-1].member.mention} 🤝"
|
711
|
+
icon_url = self.bot.app_emojis["end"].url
|
712
|
+
color = RAND.choice(COLORS)
|
713
|
+
await self.set_winner(None)
|
714
|
+
else:
|
715
|
+
winner = best_players[0]
|
716
|
+
content = f"## Gagnant {winner.member.mention} 🎉"
|
717
|
+
color = COLORS[self.players.index(winner)]
|
718
|
+
icon_url = winner.member.display_avatar.url
|
719
|
+
await self.set_winner(winner)
|
720
|
+
|
721
|
+
# The game is starting
|
722
|
+
elif self.starting:
|
723
|
+
icon_url = self.bot.app_emojis["wait"].url
|
724
|
+
color = RAND.choice(COLORS)
|
725
|
+
content = "Retournez chacun 2 cartes.\n"
|
726
|
+
content += f"-# Fin du tour {self.in_seconds}"
|
727
|
+
|
728
|
+
# Normal play
|
729
|
+
else:
|
730
|
+
icon_url = self.current_player.member.display_avatar.url
|
731
|
+
color = COLORS[self.turn % len(self.players)]
|
732
|
+
content = f"Tour n°{self.turn + 1}\n"
|
733
|
+
content += "Joueur actuel : "
|
734
|
+
content += f"{self.current_player.member.mention}\n"
|
735
|
+
if self.finish_turn:
|
736
|
+
content += "**Attention, c'est le dernier tour !**\n"
|
737
|
+
if self.turn_state == TurnState.START:
|
738
|
+
content += (
|
739
|
+
f"Piochez une carte ou prenez {self.current_card.emoji}\n"
|
740
|
+
)
|
741
|
+
else:
|
742
|
+
content += f"Vous avez pioché {self.current_card.emoji}\n"
|
743
|
+
content += "Remplacez une carte ou retournez-en une.\n"
|
744
|
+
if self.turn_state == TurnState.START:
|
745
|
+
await self.stop_timer()
|
746
|
+
await self.start_timer(
|
747
|
+
self.timeout_penalty.get(
|
748
|
+
self.current_player,
|
749
|
+
60,
|
750
|
+
)
|
751
|
+
)
|
752
|
+
content += f"-# Fin du tour {self.in_seconds}"
|
753
|
+
|
754
|
+
# Show player grid
|
755
|
+
for p in self.players:
|
756
|
+
grid = self.grids[p]
|
757
|
+
score = scores[p] if scores else grid.value_visible
|
758
|
+
embed.add_field(
|
759
|
+
name=p.member.display_name,
|
760
|
+
value=f"-# {score} points\n{grid}",
|
761
|
+
inline=False,
|
762
|
+
)
|
763
|
+
|
764
|
+
# Update view
|
765
|
+
options: dict[str, Any] = {}
|
766
|
+
if not self.starting or all(
|
767
|
+
c.hidden for g in self.grids.values() for c in g.cards.values()
|
768
|
+
):
|
769
|
+
options["view"] = ActionView(self)
|
770
|
+
if scores:
|
771
|
+
options["view"] = None
|
772
|
+
|
773
|
+
# Update embed
|
774
|
+
logger.info("Update display")
|
775
|
+
embed.set_author(name=header, icon_url=icon_url)
|
776
|
+
embed.description = content
|
777
|
+
embed.colour = color # type: ignore[assignment]
|
778
|
+
await self.message.edit(
|
779
|
+
content=f"-# {' '.join(p.member.mention for p in self.players)}",
|
780
|
+
embed=embed,
|
781
|
+
**options,
|
782
|
+
)
|
783
|
+
|
784
|
+
def scores(self) -> dict[Player, int]:
|
785
|
+
"""Get all score."""
|
786
|
+
scores = {}
|
787
|
+
for p, grid in self.grids.items():
|
788
|
+
scores[p] = grid.value
|
789
|
+
grid.return_all_card()
|
790
|
+
finish_player = self.finish_player
|
791
|
+
if finish_player:
|
792
|
+
finish_score = scores[finish_player]
|
793
|
+
for p, value in scores.items():
|
794
|
+
if p != finish_player and value <= finish_score:
|
795
|
+
scores[finish_player] *= 2
|
796
|
+
break
|
797
|
+
return scores
|
798
|
+
|
799
|
+
@override
|
800
|
+
async def on_timeout(self) -> None:
|
801
|
+
if self.starting:
|
802
|
+
await AI(self).random_starting()
|
803
|
+
else:
|
804
|
+
self.timeout_penalty[self.current_player] = 20
|
805
|
+
await AI(self).play()
|