1use crate::{Category, Dice, Keep, ScoreDelta, Scorecard, TurnAction, View};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum Phase {
8 AwaitingRoll,
12 AwaitingAction,
14 Finished,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
20#[non_exhaustive]
21pub enum GameError {
22 #[error("dice must be rolled before acting")]
24 RollPending,
25 #[error("an action is pending; the dice have already been rolled")]
27 ActionPending,
28 #[error("roll does not contain the held dice {kept}")]
30 KeptNotHeld {
31 kept: Keep,
33 },
34 #[error("keep {keep} is not part of the current dice")]
36 KeepNotInDice {
37 keep: Keep,
39 },
40 #[error("no rerolls left this turn")]
42 NoRerollsLeft,
43 #[error("cannot score {category} here")]
45 IllegalCategory {
46 category: Category,
48 },
49 #[error("the game is finished")]
51 Finished,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Hash)]
65pub struct Game {
66 scorecards: Vec<Scorecard>,
67 seat: usize,
68 round: u8,
69 kept: Keep,
70 dice: Option<Dice>,
71 rolls_used: u8,
72}
73
74impl Game {
75 #[must_use]
81 pub fn new(players: usize) -> Self {
82 assert!(players > 0, "a game needs at least one player");
83 Self {
84 scorecards: vec![Scorecard::new(); players],
85 seat: 0,
86 round: 0,
87 kept: Keep::EMPTY,
88 dice: None,
89 rolls_used: 0,
90 }
91 }
92
93 #[must_use]
95 pub const fn phase(&self) -> Phase {
96 if self.round >= 13 {
97 Phase::Finished
98 } else if self.dice.is_none() {
99 Phase::AwaitingRoll
100 } else {
101 Phase::AwaitingAction
102 }
103 }
104
105 #[must_use]
107 pub const fn is_over(&self) -> bool {
108 self.round >= 13
109 }
110
111 #[must_use]
113 pub const fn seat(&self) -> usize {
114 self.seat
115 }
116
117 #[must_use]
119 pub const fn round(&self) -> u8 {
120 self.round
121 }
122
123 #[must_use]
126 pub const fn rolls_left(&self) -> u8 {
127 if self.is_over() {
128 0
129 } else {
130 3 - self.rolls_used
131 }
132 }
133
134 #[must_use]
136 pub const fn kept(&self) -> Keep {
137 self.kept
138 }
139
140 #[must_use]
142 pub const fn dice(&self) -> Option<Dice> {
143 self.dice
144 }
145
146 #[must_use]
152 pub fn scorecard(&self, seat: usize) -> &Scorecard {
153 &self.scorecards[seat]
154 }
155
156 #[must_use]
158 pub fn scorecards(&self) -> &[Scorecard] {
159 &self.scorecards
160 }
161
162 #[must_use]
164 pub fn totals(&self) -> Vec<u16> {
165 self.scorecards.iter().map(|card| card.total()).collect()
166 }
167
168 #[must_use]
170 pub const fn view(&self) -> View<'_> {
171 View::new(self)
172 }
173
174 pub fn roll(&mut self, dice: Dice) -> Result<(), GameError> {
182 match self.phase() {
183 Phase::Finished => Err(GameError::Finished),
184 Phase::AwaitingAction => Err(GameError::ActionPending),
185 Phase::AwaitingRoll if !dice.contains(self.kept) => {
186 Err(GameError::KeptNotHeld { kept: self.kept })
187 }
188 Phase::AwaitingRoll => {
189 self.dice = Some(dice);
190 self.rolls_used += 1;
191 Ok(())
192 }
193 }
194 }
195
196 #[cfg(feature = "rand")]
202 pub fn roll_with<R: rand::Rng + ?Sized>(&mut self, rng: &mut R) -> Result<Dice, GameError> {
203 use rand::RngExt as _;
204 let mut counts = self.kept.counts();
205 for _ in self.kept.len()..5 {
206 counts[rng.random_range(0..6)] += 1;
207 }
208 let dice = Dice::from_counts(counts).expect("kept dice plus rerolls make five");
209 self.roll(dice).map(|()| dice)
210 }
211
212 pub fn act(&mut self, action: TurnAction) -> Result<Option<ScoreDelta>, GameError> {
223 let dice = match self.phase() {
224 Phase::Finished => return Err(GameError::Finished),
225 Phase::AwaitingRoll => return Err(GameError::RollPending),
226 Phase::AwaitingAction => self.dice.expect("dice are showing"),
227 };
228 match action {
229 TurnAction::Reroll(_) if self.rolls_left() == 0 => Err(GameError::NoRerollsLeft),
230 TurnAction::Reroll(keep) if !dice.contains(keep) => {
231 Err(GameError::KeepNotInDice { keep })
232 }
233 TurnAction::Reroll(keep) => {
234 self.kept = keep;
235 self.dice = None;
236 Ok(None)
237 }
238 TurnAction::Score(category) => {
239 let delta = self.scorecards[self.seat].score(category, dice)?;
240 self.kept = Keep::EMPTY;
241 self.dice = None;
242 self.rolls_used = 0;
243 self.seat += 1;
244 if self.seat == self.scorecards.len() {
245 self.seat = 0;
246 self.round += 1;
247 }
248 Ok(Some(delta))
249 }
250 }
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 fn dice(s: &str) -> Dice {
259 s.parse().expect("a valid roll")
260 }
261
262 fn keep(s: &str) -> Keep {
263 s.parse().expect("a valid keep")
264 }
265
266 #[test]
267 fn a_turn_runs_roll_keep_roll_keep_roll_score() {
268 let mut game = Game::new(1);
269 assert_eq!(game.phase(), Phase::AwaitingRoll);
270 assert_eq!(game.rolls_left(), 3);
271 game.roll(dice("13446")).expect("first roll");
272 assert_eq!(game.rolls_left(), 2);
273 game.act(TurnAction::Reroll(keep("44")))
274 .expect("hold fours");
275 assert_eq!(game.phase(), Phase::AwaitingRoll);
276 game.roll(dice("24445")).expect("second roll");
277 game.act(TurnAction::Reroll(keep("444")))
278 .expect("hold fours");
279 game.roll(dice("14446")).expect("third roll");
280 assert_eq!(game.rolls_left(), 0);
281 let delta = game
282 .act(TurnAction::Score(Category::FourOfAKind))
283 .expect("scoring is always legal");
284 assert_eq!(delta.expect("a score").value, 0);
285 assert_eq!(game.round(), 1);
286 assert_eq!(game.phase(), Phase::AwaitingRoll);
287 }
288
289 #[test]
290 fn rejections_leave_the_game_unchanged() {
291 let mut game = Game::new(1);
292 assert_eq!(
293 game.act(TurnAction::Score(Category::Chance)),
294 Err(GameError::RollPending)
295 );
296 game.roll(dice("13446")).expect("first roll");
297 let before = game.clone();
298 assert_eq!(game.roll(dice("13446")), Err(GameError::ActionPending));
299 assert_eq!(
300 game.act(TurnAction::Reroll(keep("55"))),
301 Err(GameError::KeepNotInDice { keep: keep("55") })
302 );
303 assert_eq!(game, before);
304
305 game.act(TurnAction::Reroll(keep("446"))).expect("legal");
306 assert_eq!(
307 game.roll(dice("11123")),
308 Err(GameError::KeptNotHeld { kept: keep("446") })
309 );
310
311 game.roll(dice("44612")).expect("second roll");
312 game.act(TurnAction::Reroll(keep("446"))).expect("legal");
313 game.roll(dice("44613")).expect("third roll");
314 assert_eq!(
315 game.act(TurnAction::Reroll(Keep::EMPTY)),
316 Err(GameError::NoRerollsLeft)
317 );
318 }
319
320 #[test]
321 fn players_alternate_and_the_game_ends_after_13_rounds() {
322 let mut game = Game::new(2);
323 for round in 0..13 {
324 for seat in 0..2 {
325 assert_eq!(game.seat(), seat);
326 assert_eq!(game.round(), round);
327 game.roll(dice("13446")).expect("a roll");
328 let category = Category::ALL[usize::from(round)];
329 game.act(TurnAction::Score(category)).expect("open box");
330 }
331 }
332 assert!(game.is_over());
333 assert_eq!(game.phase(), Phase::Finished);
334 assert_eq!(game.rolls_left(), 0);
335 assert_eq!(game.roll(dice("13446")), Err(GameError::Finished));
336 assert_eq!(game.totals().len(), 2);
337 }
338}