2021-11-08 14:44:03 +00:00
|
|
|
import pickle
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
from enum import Enum
|
2021-11-12 13:32:02 +00:00
|
|
|
from pathlib import Path
|
2021-11-08 20:18:48 +00:00
|
|
|
from typing import ClassVar, Iterable, Generator
|
2021-11-08 14:44:03 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Room(Enum):
|
|
|
|
A = 'A'
|
|
|
|
B = 'B'
|
|
|
|
C = 'C'
|
|
|
|
|
|
|
|
|
2021-11-08 15:29:50 +00:00
|
|
|
class GameState(Enum):
|
|
|
|
PREPARING = 0
|
|
|
|
VOTING = 1
|
|
|
|
RESULTS = 2
|
|
|
|
|
|
|
|
|
2021-11-08 14:44:03 +00:00
|
|
|
class Vote(Enum):
|
|
|
|
ALLY = 'A'
|
|
|
|
BETRAY = 'B'
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
class Player:
|
|
|
|
name: str
|
|
|
|
private_channel_id: int = field(hash=False)
|
|
|
|
|
|
|
|
@property
|
2021-11-08 20:18:48 +00:00
|
|
|
def round_votes(self) -> Generator["RoundVote", None, None]:
|
2021-11-08 14:44:03 +00:00
|
|
|
for r in Game.INSTANCE.rounds:
|
|
|
|
for room in r.rooms:
|
|
|
|
for vote in room.votes:
|
|
|
|
if self in vote.players:
|
|
|
|
yield vote
|
|
|
|
|
|
|
|
@property
|
2021-11-12 13:26:01 +00:00
|
|
|
def calculated_score(self) -> int:
|
2021-11-08 14:44:03 +00:00
|
|
|
s = 3
|
|
|
|
|
|
|
|
for vote in self.round_votes:
|
2021-11-08 21:10:08 +00:00
|
|
|
if vote.room.round.round == len(Game.INSTANCE.rounds) and Game.INSTANCE.state != GameState.RESULTS:
|
|
|
|
# Don't compute temporary scores
|
|
|
|
break
|
|
|
|
|
2021-11-08 14:44:03 +00:00
|
|
|
room = vote.room
|
|
|
|
other_vote = room.vote1 if room.vote1 is not vote else room.vote2
|
|
|
|
match vote.vote, other_vote.vote:
|
|
|
|
case Vote.ALLY, Vote.ALLY:
|
|
|
|
s += 2
|
|
|
|
case Vote.ALLY, Vote.BETRAY:
|
|
|
|
s -= 2
|
|
|
|
case Vote.BETRAY, Vote.ALLY:
|
|
|
|
s += 3
|
|
|
|
case Vote.BETRAY, Vote.BETRAY:
|
|
|
|
pass
|
|
|
|
|
2021-11-08 21:10:08 +00:00
|
|
|
if s <= 0:
|
|
|
|
# Player died
|
|
|
|
return s
|
|
|
|
|
2021-11-08 14:44:03 +00:00
|
|
|
return s
|
|
|
|
|
2021-11-12 13:26:01 +00:00
|
|
|
@property
|
|
|
|
def score(self) -> int:
|
|
|
|
if self in Game.INSTANCE.score_overrides:
|
|
|
|
return Game.INSTANCE.score_overrides[self]
|
|
|
|
return self.calculated_score
|
|
|
|
|
2021-11-08 23:29:07 +00:00
|
|
|
def __str__(self):
|
|
|
|
return self.name
|
|
|
|
|
2021-11-08 14:44:03 +00:00
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class RoundVote:
|
2021-11-08 23:29:07 +00:00
|
|
|
player1: Player | None = None
|
2021-11-08 14:44:03 +00:00
|
|
|
player2: Player | None = None
|
|
|
|
vote: Vote | None = None
|
2021-11-12 13:15:39 +00:00
|
|
|
swapped: bool = field(default=False, init=False)
|
2021-11-08 14:44:03 +00:00
|
|
|
|
|
|
|
@property
|
2021-11-08 20:18:48 +00:00
|
|
|
def players(self) -> Iterable[Player]:
|
|
|
|
if self.player2 is None:
|
|
|
|
return self.player1,
|
2021-11-08 14:44:03 +00:00
|
|
|
return self.player1, self.player2
|
|
|
|
|
|
|
|
@property
|
2021-11-08 20:18:48 +00:00
|
|
|
def room(self) -> "RoundRoom":
|
2021-11-08 16:39:01 +00:00
|
|
|
for r in Game.INSTANCE.rounds:
|
2021-11-08 14:44:03 +00:00
|
|
|
for room in r.rooms:
|
|
|
|
if self in room.votes:
|
|
|
|
return room
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class RoundRoom:
|
|
|
|
room: Room
|
|
|
|
vote1: RoundVote
|
|
|
|
vote2: RoundVote
|
|
|
|
|
|
|
|
@property
|
|
|
|
def votes(self):
|
|
|
|
return self.vote1, self.vote2
|
|
|
|
|
2021-11-08 20:18:48 +00:00
|
|
|
@property
|
|
|
|
def players(self) -> Generator[Player, None, None]:
|
|
|
|
for vote in self.votes:
|
|
|
|
yield from vote.players
|
|
|
|
|
2021-11-08 14:44:03 +00:00
|
|
|
@property
|
|
|
|
def round(self):
|
|
|
|
for r in Game.INSTANCE.rounds:
|
|
|
|
if self in r.rooms:
|
|
|
|
return r
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class Round:
|
|
|
|
round: int
|
|
|
|
room_a: RoundRoom
|
|
|
|
room_b: RoundRoom
|
|
|
|
room_c: RoundRoom
|
|
|
|
|
|
|
|
@property
|
2021-11-08 20:18:48 +00:00
|
|
|
def rooms(self) -> tuple[RoundRoom, RoundRoom, RoundRoom]:
|
2021-11-08 14:44:03 +00:00
|
|
|
return self.room_a, self.room_b, self.room_c
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class Game:
|
|
|
|
INSTANCE: ClassVar["Game"] = None
|
|
|
|
|
2021-11-08 15:29:50 +00:00
|
|
|
state: GameState = GameState.PREPARING
|
2021-11-08 14:44:03 +00:00
|
|
|
rounds: list[Round] = field(default_factory=list)
|
|
|
|
players: dict[str, Player] = field(default_factory=dict)
|
2021-11-12 13:26:01 +00:00
|
|
|
score_overrides: dict[Player, int] = field(default_factory=dict, init=False)
|
2021-11-08 14:44:03 +00:00
|
|
|
|
2021-11-12 13:26:01 +00:00
|
|
|
def __post_init__(self: "Game") -> None:
|
2021-11-08 14:44:03 +00:00
|
|
|
Game.INSTANCE = self
|
|
|
|
|
|
|
|
def register_player(self, name: str, vote_channel_id: int) -> Player:
|
|
|
|
player = Player(name, vote_channel_id)
|
|
|
|
self.players[name] = player
|
|
|
|
return player
|
|
|
|
|
|
|
|
def default_first_round(self) -> Round:
|
|
|
|
return Round(
|
|
|
|
round=1,
|
|
|
|
room_a=RoundRoom(room=Room.A,
|
|
|
|
vote1=RoundVote(player1=self.players['Tora']),
|
|
|
|
vote2=RoundVote(player1=self.players['Kamui'],
|
|
|
|
player2=self.players['Philia'])),
|
2021-11-08 16:39:01 +00:00
|
|
|
room_b=RoundRoom(room=Room.B,
|
2021-11-08 14:44:03 +00:00
|
|
|
vote1=RoundVote(player1=self.players['Dan']),
|
|
|
|
vote2=RoundVote(player1=self.players['Ennea'],
|
|
|
|
player2=self.players['Delphine'])),
|
2021-11-08 16:39:01 +00:00
|
|
|
room_c=RoundRoom(room=Room.C,
|
2021-11-08 14:44:03 +00:00
|
|
|
vote1=RoundVote(player1=self.players['Hanabi']),
|
|
|
|
vote2=RoundVote(player1=self.players['Nona'],
|
|
|
|
player2=self.players['Oji'])),
|
|
|
|
)
|
|
|
|
|
2021-11-12 13:32:02 +00:00
|
|
|
def save(self, filename: str | None = None) -> None:
|
2021-11-08 14:44:03 +00:00
|
|
|
"""
|
|
|
|
Uses pickle to save the current state of the game.
|
|
|
|
"""
|
2021-11-12 13:32:02 +00:00
|
|
|
if filename is None:
|
|
|
|
filename = Path(__file__).parent.parent / 'game.save'
|
2021-11-08 14:44:03 +00:00
|
|
|
with open(filename, 'wb') as f:
|
|
|
|
pickle.dump(self, f)
|
|
|
|
|
|
|
|
@classmethod
|
2021-11-12 13:32:02 +00:00
|
|
|
def load(cls, filename: str | None = None) -> "Game | None":
|
2021-11-08 14:44:03 +00:00
|
|
|
"""
|
|
|
|
Reload the game from a saved file.
|
|
|
|
"""
|
2021-11-12 13:32:02 +00:00
|
|
|
if filename is None:
|
|
|
|
filename = Path(__file__).parent.parent / 'game.save'
|
2021-11-08 14:44:03 +00:00
|
|
|
try:
|
|
|
|
with open(filename, 'rb') as f:
|
|
|
|
game = pickle.load(f)
|
|
|
|
Game.INSTANCE = game
|
|
|
|
return game
|
|
|
|
except FileNotFoundError:
|
|
|
|
return None
|