easterobot 1.3.2__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.
Files changed (81) hide show
  1. easterobot/bot.py +14 -1
  2. easterobot/casino/__init__.py +1 -0
  3. easterobot/casino/roulette.py +269 -0
  4. easterobot/commands/__init__.py +2 -0
  5. easterobot/commands/game.py +127 -118
  6. easterobot/commands/reset.py +11 -14
  7. easterobot/commands/roulette.py +34 -0
  8. easterobot/commands/top.py +73 -65
  9. easterobot/config.py +35 -8
  10. easterobot/games/{connect.py → connect4.py} +25 -28
  11. easterobot/games/game.py +126 -54
  12. easterobot/games/rock_paper_scissor.py +33 -30
  13. easterobot/games/skyjo.py +805 -0
  14. easterobot/games/tic_tac_toe.py +19 -18
  15. easterobot/hunts/hunt.py +49 -18
  16. easterobot/hunts/rank.py +24 -2
  17. easterobot/locker.py +180 -0
  18. easterobot/models.py +9 -0
  19. easterobot/resources/config.example.yml +8 -2
  20. easterobot/resources/credits.txt +2 -0
  21. easterobot/resources/emotes/placements/s1.png +0 -0
  22. easterobot/resources/emotes/placements/s10.png +0 -0
  23. easterobot/resources/emotes/placements/s11.png +0 -0
  24. easterobot/resources/emotes/placements/s12.png +0 -0
  25. easterobot/resources/emotes/placements/s2.png +0 -0
  26. easterobot/resources/emotes/placements/s3.png +0 -0
  27. easterobot/resources/emotes/placements/s4.png +0 -0
  28. easterobot/resources/emotes/placements/s5.png +0 -0
  29. easterobot/resources/emotes/placements/s6.png +0 -0
  30. easterobot/resources/emotes/placements/s7.png +0 -0
  31. easterobot/resources/emotes/placements/s8.png +0 -0
  32. easterobot/resources/emotes/placements/s9.png +0 -0
  33. easterobot/resources/emotes/placements/sA.png +0 -0
  34. easterobot/resources/emotes/placements/sB.png +0 -0
  35. easterobot/resources/emotes/placements/sC.png +0 -0
  36. easterobot/resources/emotes/placements/sD.png +0 -0
  37. easterobot/resources/emotes/placements/sE.png +0 -0
  38. easterobot/resources/emotes/placements/sF.png +0 -0
  39. easterobot/resources/emotes/placements/sG.png +0 -0
  40. easterobot/resources/emotes/placements/sH.png +0 -0
  41. easterobot/resources/emotes/placements/sI.png +0 -0
  42. easterobot/resources/emotes/placements/sJ.png +0 -0
  43. easterobot/resources/emotes/placements/sK.png +0 -0
  44. easterobot/resources/emotes/placements/sL.png +0 -0
  45. easterobot/resources/emotes/placements/sM.png +0 -0
  46. easterobot/resources/emotes/placements/sN.png +0 -0
  47. easterobot/resources/emotes/placements/sO.png +0 -0
  48. easterobot/resources/emotes/placements/sP.png +0 -0
  49. easterobot/resources/emotes/placements/sQ.png +0 -0
  50. easterobot/resources/emotes/placements/sR.png +0 -0
  51. easterobot/resources/emotes/placements/sS.png +0 -0
  52. easterobot/resources/emotes/placements/sT.png +0 -0
  53. easterobot/resources/emotes/placements/sU.png +0 -0
  54. easterobot/resources/emotes/placements/sV.png +0 -0
  55. easterobot/resources/emotes/placements/sW.png +0 -0
  56. easterobot/resources/emotes/placements/sX.png +0 -0
  57. easterobot/resources/emotes/placements/sY.png +0 -0
  58. easterobot/resources/emotes/placements/sZ.png +0 -0
  59. easterobot/resources/emotes/placements/s_.png +0 -0
  60. easterobot/resources/emotes/skyjo/skyjo_back.png +0 -0
  61. easterobot/resources/emotes/skyjo/skyjo_m1.png +0 -0
  62. easterobot/resources/emotes/skyjo/skyjo_m2.png +0 -0
  63. easterobot/resources/emotes/skyjo/skyjo_p0.png +0 -0
  64. easterobot/resources/emotes/skyjo/skyjo_p1.png +0 -0
  65. easterobot/resources/emotes/skyjo/skyjo_p10.png +0 -0
  66. easterobot/resources/emotes/skyjo/skyjo_p11.png +0 -0
  67. easterobot/resources/emotes/skyjo/skyjo_p12.png +0 -0
  68. easterobot/resources/emotes/skyjo/skyjo_p2.png +0 -0
  69. easterobot/resources/emotes/skyjo/skyjo_p3.png +0 -0
  70. easterobot/resources/emotes/skyjo/skyjo_p4.png +0 -0
  71. easterobot/resources/emotes/skyjo/skyjo_p5.png +0 -0
  72. easterobot/resources/emotes/skyjo/skyjo_p6.png +0 -0
  73. easterobot/resources/emotes/skyjo/skyjo_p7.png +0 -0
  74. easterobot/resources/emotes/skyjo/skyjo_p8.png +0 -0
  75. easterobot/resources/emotes/skyjo/skyjo_p9.png +0 -0
  76. {easterobot-1.3.2.dist-info → easterobot-1.5.1.dist-info}/METADATA +1 -1
  77. easterobot-1.5.1.dist-info/RECORD +130 -0
  78. easterobot-1.3.2.dist-info/RECORD +0 -70
  79. {easterobot-1.3.2.dist-info → easterobot-1.5.1.dist-info}/WHEEL +0 -0
  80. {easterobot-1.3.2.dist-info → easterobot-1.5.1.dist-info}/entry_points.txt +0 -0
  81. {easterobot-1.3.2.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()