]>
Commit | Line | Data |
---|---|---|
b6963095 | 1 | use rand::seq::SliceRandom; |
57f490a0 SW |
2 | use rand::Rng; |
3 | ||
95b540e1 SW |
4 | pub const NUM_RANKS: usize = 13; |
5 | pub const NUM_SUITS: usize = 4; | |
6 | pub const NUM_JOKERS: usize = 2; | |
7 | pub const NUM_CARDS: usize = NUM_RANKS * NUM_SUITS + NUM_JOKERS; | |
57f490a0 | 8 | |
b6963095 SW |
9 | pub const STARTING_CARDS: u8 = 3; |
10 | pub const STARTING_MAD_SCIENCE_TOKENS: i8 = 15; | |
11 | pub const STARTING_PROGRESS: i8 = -10; | |
12 | ||
13 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] | |
57f490a0 | 14 | pub struct Rank(u8); |
b8e315ac SW |
15 | impl Rank { |
16 | #[must_use] | |
17 | pub fn value(&self) -> u8 { | |
18 | self.0 + 1 | |
19 | } | |
b6963095 SW |
20 | #[must_use] |
21 | pub fn is_face(&self) -> bool { | |
22 | self.value() > 10 | |
23 | } | |
24 | #[must_use] | |
25 | pub fn random() -> Self { | |
26 | Self( | |
27 | rand::thread_rng() | |
28 | .gen_range(0..NUM_RANKS) | |
29 | .try_into() | |
30 | .expect("Too many ranks?"), | |
31 | ) | |
32 | } | |
b8e315ac | 33 | } |
57f490a0 | 34 | |
09822a98 SW |
35 | #[derive(Clone, Copy, Eq, PartialEq)] |
36 | pub struct Suit(u8); | |
37 | ||
644e6c7a | 38 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] |
09822a98 SW |
39 | pub struct Card(u8); |
40 | impl Card { | |
41 | #[must_use] | |
10e7da7b | 42 | pub fn is_joker(&self) -> bool { |
95b540e1 | 43 | usize::from(self.0) >= NUM_RANKS * NUM_SUITS |
09822a98 SW |
44 | } |
45 | #[must_use] | |
10e7da7b SW |
46 | pub fn rank(&self) -> Option<Rank> { |
47 | (!self.is_joker()).then_some(Rank(self.0 >> 2)) | |
09822a98 | 48 | } |
10e7da7b SW |
49 | #[must_use] |
50 | pub fn suit(&self) -> Option<Suit> { | |
51 | (!self.is_joker()).then_some(Suit(self.0 & 3)) | |
52 | } | |
53 | } | |
54 | ||
55 | #[derive(Clone, Copy)] | |
56 | pub enum WithOrWithoutJokers { | |
57 | WithJokers, | |
58 | WithoutJokers, | |
09822a98 SW |
59 | } |
60 | ||
61 | #[must_use] | |
10e7da7b | 62 | pub fn deck(j: WithOrWithoutJokers) -> Vec<Card> { |
95b540e1 | 63 | let limit = u8::try_from(match j { |
10e7da7b SW |
64 | WithOrWithoutJokers::WithJokers => NUM_CARDS, |
65 | WithOrWithoutJokers::WithoutJokers => NUM_SUITS * NUM_RANKS, | |
95b540e1 SW |
66 | }) |
67 | .expect("Too many cards?"); | |
10e7da7b | 68 | (0..limit).map(Card).collect() |
09822a98 SW |
69 | } |
70 | ||
b6963095 SW |
71 | fn shuffle(cards: &mut Vec<Card>) { |
72 | cards.shuffle(&mut rand::thread_rng()); | |
73 | } | |
74 | #[must_use] | |
75 | fn shuffled(mut cards: Vec<Card>) -> Vec<Card> { | |
76 | shuffle(&mut cards); | |
77 | cards | |
78 | } | |
79 | ||
80 | #[derive(Clone, Copy, Debug)] | |
57f490a0 | 81 | pub struct PathLength(Rank); |
b6963095 SW |
82 | impl PathLength { |
83 | #[must_use] | |
84 | pub fn random() -> Self { | |
85 | Self(Rank::random()) | |
86 | } | |
87 | } | |
57f490a0 SW |
88 | |
89 | #[derive(Clone, Copy, Default)] | |
90 | pub struct PathLengthInfo(u16); | |
91 | impl PathLengthInfo { | |
92 | #[must_use] | |
93 | pub fn is_showing(&self, i: Rank) -> bool { | |
94 | (self.0 >> i.0) & 1 == 1 | |
95 | } | |
96 | fn reveal(&mut self, i: Rank) { | |
97 | self.0 |= 1 << i.0; | |
98 | } | |
99 | pub fn reveal_random(&mut self, true_length: PathLength) -> Option<Rank> { | |
95b540e1 | 100 | let showing = usize::try_from(self.0.count_ones()).expect("There aren't that many bits"); |
57f490a0 SW |
101 | let not_showing = NUM_RANKS - showing; |
102 | if not_showing <= 1 { | |
103 | return None; | |
104 | } | |
105 | ||
106 | let mut show = rand::thread_rng().gen_range(0..not_showing - 1); | |
107 | for i in 0..NUM_RANKS { | |
95b540e1 | 108 | let r = Rank(u8::try_from(i).expect("Too many cards?")); |
57f490a0 SW |
109 | if !self.is_showing(r) && r != true_length.0 { |
110 | if show == 0 { | |
111 | self.reveal(r); | |
112 | return Some(r); | |
113 | } | |
114 | show -= 1; | |
115 | } | |
116 | } | |
117 | unreachable!() | |
118 | } | |
754e9730 SW |
119 | } |
120 | ||
644e6c7a SW |
121 | #[derive(Default)] |
122 | pub struct Discard { | |
123 | cards: Vec<Card>, | |
124 | } | |
125 | impl Discard { | |
126 | pub fn discard(&mut self, card: Card) { | |
127 | self.cards.push(card); | |
128 | } | |
b6963095 SW |
129 | #[must_use] |
130 | pub fn top(&self) -> Option<&Card> { | |
131 | self.cards.last() | |
132 | } | |
133 | fn len(&self) -> usize { | |
134 | self.cards.len() | |
135 | } | |
644e6c7a SW |
136 | } |
137 | ||
138 | pub struct Library { | |
139 | cards: Vec<Card>, | |
140 | } | |
141 | impl Library { | |
142 | #[must_use] | |
143 | pub fn new(cards: Vec<Card>) -> Self { | |
144 | Self { cards } | |
145 | } | |
146 | pub fn draw(&mut self, discard: &mut Discard) -> Option<Card> { | |
147 | if self.cards.is_empty() { | |
148 | if let Some(top_discard) = discard.cards.pop() { | |
149 | std::mem::swap(&mut self.cards, &mut discard.cards); | |
150 | discard.discard(top_discard); | |
b6963095 | 151 | shuffle(&mut self.cards); |
644e6c7a SW |
152 | } |
153 | } | |
154 | self.cards.pop() | |
155 | } | |
b6963095 SW |
156 | fn len(&self) -> usize { |
157 | self.cards.len() | |
158 | } | |
644e6c7a SW |
159 | } |
160 | ||
b6963095 SW |
161 | #[derive(Debug, Default)] |
162 | pub struct Hand { | |
b255b7c6 SW |
163 | cards: Vec<Card>, |
164 | } | |
b255b7c6 SW |
165 | impl Hand { |
166 | fn add(&mut self, card: Card) { | |
167 | self.cards.push(card); | |
168 | } | |
169 | fn remove(&mut self, card: Card) -> Result<(), &'static str> { | |
170 | let i = self | |
171 | .cards | |
172 | .iter() | |
173 | .position(|&e| e == card) | |
174 | .ok_or("That card is not in your hand")?; | |
175 | self.cards.swap_remove(i); | |
176 | Ok(()) | |
177 | } | |
b6963095 SW |
178 | fn len(&self) -> usize { |
179 | self.cards.len() | |
180 | } | |
b6963095 SW |
181 | fn random(&self) -> Option<&Card> { |
182 | self.cards.choose(&mut rand::thread_rng()) | |
183 | } | |
184 | } | |
185 | ||
186 | #[derive(Copy, Clone)] | |
187 | pub struct PlayerIndex(usize); | |
188 | impl PlayerIndex { | |
189 | fn next(self, num_players: usize) -> Self { | |
190 | Self((self.0 + 1) % num_players) | |
191 | } | |
192 | } | |
193 | ||
194 | #[derive(Copy, Clone, Debug)] | |
195 | pub enum Play { | |
196 | Play(Card), | |
197 | Draw, | |
198 | } | |
199 | ||
200 | #[derive(Eq, PartialEq)] | |
201 | pub enum Phase { | |
202 | Play, | |
203 | Momentum, | |
204 | } | |
205 | ||
cc2b69f3 | 206 | #[derive(Debug)] |
b6963095 SW |
207 | pub enum GameOutcome { |
208 | Loss, | |
209 | Win, | |
210 | } | |
211 | ||
212 | pub enum PlayOutcome { | |
213 | Continue, | |
214 | End(GameOutcome), | |
215 | } | |
216 | ||
217 | pub struct Game { | |
218 | mad_science_tokens: i8, | |
219 | progress: [i8; NUM_SUITS], | |
220 | path_lengths: [PathLength; NUM_SUITS], | |
221 | path_length_info: [PathLengthInfo; NUM_SUITS], | |
222 | library: Library, | |
223 | discard: Discard, | |
224 | hands: Vec<Hand>, | |
225 | turn: PlayerIndex, | |
226 | phase: Phase, | |
227 | } | |
228 | impl Game { | |
229 | pub fn add_player(&mut self) { | |
230 | self.hands.push(Hand::default()); | |
231 | for _ in 0..STARTING_CARDS { | |
232 | self.draw_for_player(PlayerIndex(self.hands.len() - 1)); | |
233 | } | |
234 | } | |
235 | /// # Errors | |
236 | /// | |
237 | /// Will return `Err` on invalid plays, like trying to draw during Play phase, | |
238 | /// or trying to play a card that's not in your hand. | |
239 | pub fn play(&mut self, play: Play) -> Result<PlayOutcome, &'static str> { | |
240 | match play { | |
241 | Play::Play(card) => self.play_card(card), | |
242 | Play::Draw => self.draw_for_momentum(), | |
243 | } | |
244 | } | |
245 | ||
246 | #[must_use] | |
247 | pub fn current_player_hand(&self) -> &Hand { | |
248 | &self.hands[self.turn.0] | |
249 | } | |
250 | fn player_hand_mut(&mut self, pi: PlayerIndex) -> &mut Hand { | |
251 | &mut self.hands[pi.0] | |
252 | } | |
253 | fn current_player_hand_mut(&mut self) -> &mut Hand { | |
254 | self.player_hand_mut(self.turn) | |
255 | } | |
256 | ||
257 | fn play_card(&mut self, card: Card) -> Result<PlayOutcome, &'static str> { | |
258 | let momentum = self.apply_card(card)?; | |
259 | if self.phase == Phase::Play && momentum { | |
260 | self.phase = Phase::Momentum; | |
261 | Ok(PlayOutcome::Continue) | |
262 | } else { | |
263 | Ok(self.end_of_turn()) | |
264 | } | |
265 | } | |
266 | fn draw_for_momentum(&mut self) -> Result<PlayOutcome, &'static str> { | |
267 | if self.phase != Phase::Momentum { | |
268 | return Err("You don't have momentum"); | |
269 | } | |
270 | self.draw_for_player(self.turn); | |
271 | Ok(self.end_of_turn()) | |
272 | } | |
273 | ||
274 | fn draw_for_player(&mut self, pi: PlayerIndex) { | |
275 | loop { | |
276 | if let Some(card) = self.library.draw(&mut self.discard) { | |
277 | if card.is_joker() { | |
278 | self.remove_mad_science_token(); | |
279 | self.discard.discard(card); | |
280 | } else { | |
281 | self.player_hand_mut(pi).add(card); | |
282 | break; | |
283 | } | |
284 | } else { | |
285 | println!("Library ran out of cards"); | |
286 | } | |
287 | } | |
288 | } | |
289 | fn remove_mad_science_token(&mut self) { | |
290 | loop { | |
291 | self.mad_science_tokens -= 1; | |
292 | if self.mad_science_tokens != 0 { | |
293 | break; | |
294 | } | |
295 | } | |
296 | } | |
297 | fn make_progress(&mut self, card: Card) { | |
298 | let rank = card.rank().expect("Can't play jokers").0; | |
299 | if rank < 6 { | |
300 | let roll = rand::thread_rng().gen_range(1..=6); | |
301 | if roll > rank { | |
302 | self.remove_mad_science_token(); | |
303 | } | |
304 | } | |
305 | self.progress[usize::from(card.suit().expect("Can't play jokers").0)] += 1; | |
306 | } | |
307 | fn forecast(&mut self, card: Card) { | |
308 | let suit = usize::from(card.suit().expect("Can't play jokers").0); | |
309 | self.path_length_info[suit].reveal_random(self.path_lengths[suit]); | |
310 | } | |
311 | // Returns whether or not this play grants momentum | |
312 | fn apply_card(&mut self, card: Card) -> Result<bool, &'static str> { | |
313 | self.current_player_hand_mut().remove(card)?; | |
314 | if card.rank().expect("Can't play jokers").is_face() { | |
315 | self.forecast(card); | |
316 | } else { | |
317 | self.make_progress(card); | |
318 | } | |
319 | let suits_match = self | |
320 | .discard | |
321 | .top() | |
322 | .map_or(false, |dis| dis.suit() == card.suit()); | |
323 | self.discard.discard(card); | |
324 | Ok(suits_match) | |
325 | } | |
326 | fn valid(&self) -> bool { | |
327 | 108 == (self.library.len() | |
328 | + self.discard.len() | |
329 | + self.hands.iter().map(Hand::len).sum::<usize>()) | |
330 | } | |
331 | fn roll_mad_science(&mut self) -> PlayOutcome { | |
332 | let mut tokens = std::iter::from_fn(|| Some(rand::thread_rng().gen_bool(0.5))) | |
333 | .take(usize::try_from(self.mad_science_tokens.abs()).expect("wat?")); | |
334 | let keep_going = if self.mad_science_tokens > 0 { | |
335 | tokens.any(|t| !t) | |
336 | } else { | |
337 | tokens.all(|t| !t) | |
338 | }; | |
339 | if keep_going { | |
340 | PlayOutcome::Continue | |
341 | } else { | |
342 | PlayOutcome::End(self.final_score()) | |
343 | } | |
344 | } | |
345 | fn final_score(&self) -> GameOutcome { | |
346 | if self | |
347 | .progress | |
348 | .iter() | |
349 | .zip(self.path_lengths.iter()) | |
350 | .any(|(&prog, len)| prog >= len.0 .0.try_into().expect("wat?")) | |
351 | { | |
352 | GameOutcome::Win | |
353 | } else { | |
354 | GameOutcome::Loss | |
355 | } | |
356 | } | |
357 | fn end_of_turn(&mut self) -> PlayOutcome { | |
358 | assert!(self.valid()); | |
359 | self.phase = Phase::Play; | |
360 | self.turn = self.turn.next(self.hands.len()); | |
361 | if self.turn.0 == 0 { | |
362 | if let PlayOutcome::End(game_outcome) = self.roll_mad_science() { | |
363 | return PlayOutcome::End(game_outcome); | |
364 | } | |
365 | } | |
366 | self.draw_for_player(self.turn); | |
367 | assert!(self.valid()); | |
368 | PlayOutcome::Continue | |
369 | } | |
370 | } | |
371 | impl Default for Game { | |
372 | fn default() -> Self { | |
373 | Self { | |
374 | mad_science_tokens: STARTING_MAD_SCIENCE_TOKENS, | |
375 | progress: [STARTING_PROGRESS; NUM_SUITS], | |
376 | path_lengths: std::iter::from_fn(|| Some(PathLength::random())) | |
377 | .take(NUM_SUITS) | |
378 | .collect::<Vec<_>>() | |
379 | .try_into() | |
380 | .expect("wat?"), | |
381 | path_length_info: [PathLengthInfo::default(); NUM_SUITS], | |
382 | library: Library::new(shuffled( | |
383 | [ | |
384 | deck(WithOrWithoutJokers::WithJokers), | |
385 | deck(WithOrWithoutJokers::WithJokers), | |
386 | ] | |
387 | .concat(), | |
388 | )), | |
389 | discard: Discard::default(), | |
390 | hands: vec![], | |
391 | turn: PlayerIndex(0), | |
392 | phase: Phase::Play, | |
393 | } | |
394 | } | |
395 | } | |
396 | ||
397 | pub struct Player(Box<dyn FnMut(&Game) -> Play>); | |
cc2b69f3 SW |
398 | impl Player { |
399 | #[must_use] | |
400 | pub fn new<T>(f: T) -> Self | |
401 | where | |
402 | T: FnMut(&Game) -> Play + 'static, | |
403 | { | |
404 | Self(Box::new(f)) | |
405 | } | |
406 | } | |
b6963095 | 407 | |
cc2b69f3 SW |
408 | #[must_use] |
409 | pub fn random_player(game: &Game) -> Play { | |
b6963095 SW |
410 | match game.phase { |
411 | Phase::Play => Play::Play( | |
412 | *game | |
413 | .current_player_hand() | |
414 | .random() | |
415 | .expect("I always have a card to play because I just drew one"), | |
416 | ), | |
417 | Phase::Momentum => { | |
418 | if rand::thread_rng().gen_bool(0.5) { | |
419 | Play::Draw | |
420 | } else { | |
421 | match game.current_player_hand().random() { | |
422 | Some(card) => Play::Play(*card), | |
423 | None => Play::Draw, | |
424 | } | |
425 | } | |
426 | } | |
427 | } | |
428 | } | |
429 | ||
430 | /// # Errors | |
431 | /// | |
432 | /// Will return `Err` on invalid plays, like trying to draw during Play phase, | |
433 | /// or trying to play a card that's not in your hand. | |
434 | pub fn play(mut game: Game, mut players: Vec<Player>) -> Result<GameOutcome, &'static str> { | |
435 | game.draw_for_player(game.turn); | |
436 | loop { | |
437 | if let PlayOutcome::End(game_outcome) = game.play(players[game.turn.0].0(&game))? { | |
438 | return Ok(game_outcome); | |
439 | } | |
440 | } | |
b255b7c6 SW |
441 | } |
442 | ||
754e9730 SW |
443 | #[cfg(test)] |
444 | mod tests { | |
445 | use super::*; | |
446 | ||
447 | #[test] | |
57f490a0 SW |
448 | fn path_length_info_random_reveal() { |
449 | let length = PathLength(Rank(7)); | |
450 | let mut pli = PathLengthInfo::default(); | |
451 | for _ in 0..12 { | |
452 | let old_pli = PathLengthInfo::clone(&pli); | |
453 | match pli.reveal_random(length) { | |
454 | None => panic!("Nothing revealed?"), | |
455 | Some(r) => { | |
456 | assert!(!old_pli.is_showing(r)); | |
457 | assert!(pli.is_showing(r)); | |
458 | } | |
459 | } | |
460 | assert_eq!(pli.0.count_ones(), 1 + old_pli.0.count_ones()); | |
461 | } | |
462 | assert!(pli.reveal_random(length).is_none()); | |
754e9730 | 463 | } |
09822a98 SW |
464 | |
465 | #[test] | |
466 | fn test_deck() { | |
10e7da7b | 467 | use WithOrWithoutJokers::*; |
b8e315ac SW |
468 | let d = deck(WithoutJokers); |
469 | let rank_sum: u32 = d | |
470 | .iter() | |
471 | .map(Card::rank) | |
472 | .flatten() | |
473 | .map(|r| u32::from(r.value())) | |
474 | .sum(); | |
475 | assert_eq!(rank_sum, 364); | |
10e7da7b | 476 | let _dj = deck(WithJokers); |
09822a98 | 477 | } |
644e6c7a SW |
478 | |
479 | #[test] | |
480 | fn test_library() { | |
481 | let mut lib = Library::new(vec![Card(7)]); | |
482 | let mut dis = Discard::default(); | |
483 | dis.discard(Card(8)); | |
484 | dis.discard(Card(9)); | |
485 | assert_eq!(lib.draw(&mut dis), Some(Card(7))); | |
486 | assert_eq!(lib.draw(&mut dis), Some(Card(8))); | |
487 | assert_eq!(lib.draw(&mut dis), None); | |
488 | } | |
b255b7c6 SW |
489 | |
490 | #[test] | |
491 | fn test_hand() { | |
492 | let mut h = Hand::default(); | |
493 | assert!(h.remove(Card(4)).is_err()); | |
494 | h.add(Card(4)); | |
495 | assert!(h.remove(Card(3)).is_err()); | |
496 | assert!(h.remove(Card(4)).is_ok()); | |
497 | assert!(h.remove(Card(4)).is_err()); | |
498 | } | |
b6963095 SW |
499 | |
500 | #[test] | |
501 | fn test_game() { | |
7d0aa2d6 | 502 | for num_players in 1..10 { |
cc2b69f3 | 503 | let players: Vec<_> = std::iter::from_fn(|| Some(Player::new(random_player))) |
7d0aa2d6 SW |
504 | .take(num_players) |
505 | .collect(); | |
506 | let mut game = Game::default(); | |
507 | for _ in 0..num_players { | |
508 | game.add_player(); | |
509 | } | |
510 | assert!(play(game, players).is_ok()); | |
511 | } | |
b6963095 | 512 | } |
754e9730 | 513 | } |