Spaces:
Running
Running
File size: 7,371 Bytes
2c6b921 741f475 2e67341 f511789 2c6b921 741f475 f511789 741f475 2c6b921 741f475 2e67341 f511789 |
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 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
"""
Devil's Dozen - Game Engine Base Classes
This module defines the foundational data structures and enums used throughout
the game engine. All classes are immutable (frozen dataclasses) to ensure
thread-safety and predictable behavior.
"""
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Sequence
class DiceType(Enum):
"""Type of dice used in the game."""
D6 = 6
D20 = 20
class GameMode(Enum):
"""Available game modes."""
PEASANTS_GAMBLE = "peasants_gamble"
ALCHEMISTS_ASCENT = "alchemists_ascent"
KNUCKLEBONES = "knucklebones"
ALIEN_INVASION = "alien_invasion"
PIG = "pig"
class Tier(Enum):
"""Tiers for Alchemist's Ascent mode."""
RED = 1 # 0-100 points, 8 dice
GREEN = 2 # 101-200 points, 3 dice
BLUE = 3 # 201-250 points, 1 die
class ScoringCategory(Enum):
"""Categories of scoring combinations."""
SINGLE_ONE = auto()
SINGLE_FIVE = auto()
THREE_OF_A_KIND = auto()
FOUR_OF_A_KIND = auto()
FIVE_OF_A_KIND = auto()
SIX_OF_A_KIND = auto()
LOW_STRAIGHT = auto() # 1-2-3-4-5
HIGH_STRAIGHT = auto() # 2-3-4-5-6
FULL_STRAIGHT = auto() # 1-2-3-4-5-6
PAIR = auto() # D20 mode
SEQUENCE = auto() # D20 mode
TIER_BONUS = auto() # D20 mode multipliers
@dataclass(frozen=True)
class ScoringBreakdown:
"""
A single scoring component within a roll.
Attributes:
category: The type of scoring combination
dice_values: The dice that contributed to this score
points: Points awarded for this combination
description: Human-readable description
"""
category: ScoringCategory
dice_values: tuple[int, ...]
points: int
description: str
@dataclass(frozen=True)
class ScoringResult:
"""
Complete scoring result for a dice roll.
Attributes:
points: Total points scored
breakdown: List of individual scoring components
scoring_dice_indices: Indices of dice that scored
is_bust: Whether no dice scored (automatic bust)
"""
points: int
breakdown: tuple[ScoringBreakdown, ...]
scoring_dice_indices: frozenset[int]
is_bust: bool = False
@property
def has_scoring_dice(self) -> bool:
"""Returns True if at least one die scored."""
return len(self.scoring_dice_indices) > 0
def __str__(self) -> str:
if self.is_bust:
return "BUST! No scoring dice."
lines = [f"Total: {self.points} points"]
for item in self.breakdown:
lines.append(f" - {item.description}: {item.points}")
return "\n".join(lines)
@dataclass(frozen=True)
class DiceRoll:
"""
Immutable representation of a dice roll.
Attributes:
values: Tuple of dice face values
dice_type: Type of dice (D6 or D20)
"""
values: tuple[int, ...]
dice_type: DiceType = DiceType.D6
def __post_init__(self) -> None:
"""Validate dice values are within valid range."""
max_value = self.dice_type.value
for value in self.values:
if not (1 <= value <= max_value):
raise ValueError(
f"Invalid die value {value} for {self.dice_type.name}. "
f"Must be between 1 and {max_value}."
)
def __len__(self) -> int:
return len(self.values)
def __getitem__(self, index: int) -> int:
return self.values[index]
@classmethod
def from_sequence(
cls,
values: Sequence[int],
dice_type: DiceType = DiceType.D6
) -> "DiceRoll":
"""Create a DiceRoll from any sequence type."""
return cls(values=tuple(values), dice_type=dice_type)
@dataclass(frozen=True)
class TurnState:
"""
Complete state of a player's turn.
Attributes:
active_dice: Current dice values in play
held_indices: Indices of dice that have been held
turn_score: Points accumulated this turn (not yet banked)
roll_count: Number of rolls taken this turn
is_hot_dice: Whether all dice have scored (can roll all again)
tier: Current tier (Alchemist's Ascent only)
previous_dice: Previous roll for reroll comparison (Tier 2)
"""
active_dice: tuple[int, ...]
held_indices: frozenset[int] = field(default_factory=frozenset)
turn_score: int = 0
roll_count: int = 0
is_hot_dice: bool = False
tier: Tier = Tier.RED
previous_dice: tuple[int, ...] = field(default_factory=tuple)
@property
def available_dice_count(self) -> int:
"""Number of dice that can still be rolled."""
return len(self.active_dice) - len(self.held_indices)
@property
def held_dice_values(self) -> tuple[int, ...]:
"""Values of the held dice."""
return tuple(self.active_dice[i] for i in sorted(self.held_indices))
@property
def unheld_dice_values(self) -> tuple[int, ...]:
"""Values of dice not yet held."""
return tuple(
v for i, v in enumerate(self.active_dice)
if i not in self.held_indices
)
@dataclass(frozen=True)
class GameConfig:
"""
Configuration for a game session.
Attributes:
mode: Game mode (Peasant's Gamble or Alchemist's Ascent)
target_score: Score needed to win
num_players: Number of players (2-4)
"""
mode: GameMode
target_score: int
num_players: int = 2
def __post_init__(self) -> None:
"""Validate configuration."""
if self.mode == GameMode.KNUCKLEBONES:
# Knucklebones is strictly 2-player
if self.num_players != 2:
raise ValueError("Knucklebones requires exactly 2 players.")
elif self.mode == GameMode.PIG:
# Pig supports 2-10 players
if not 2 <= self.num_players <= 10:
raise ValueError("Number of players for Pig must be between 2 and 10.")
else:
# Other modes support 2-4 players
if not 2 <= self.num_players <= 4:
raise ValueError("Number of players must be between 2 and 4.")
if self.mode == GameMode.PEASANTS_GAMBLE:
valid_targets = {3000, 5000, 10000}
if self.target_score not in valid_targets:
raise ValueError(
f"Target score for Peasant's Gamble must be one of {valid_targets}."
)
elif self.mode == GameMode.ALCHEMISTS_ASCENT:
if self.target_score != 250:
raise ValueError("Target score for Alchemist's Ascent must be 250.")
elif self.mode == GameMode.KNUCKLEBONES:
# Target score is not used in Knucklebones (game ends on full grid)
pass
elif self.mode == GameMode.ALIEN_INVASION:
valid_targets = {25, 50, 75}
if self.target_score not in valid_targets:
raise ValueError(
f"Target score for Alien Invasion must be one of {valid_targets}."
)
elif self.mode == GameMode.PIG:
valid_targets = {50, 100, 250}
if self.target_score not in valid_targets:
raise ValueError(
f"Target score for Pig must be one of {valid_targets}."
)
|