]>
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 | } | |
83741fed SW |
184 | /// Which suits are in this Hand? |
185 | fn suits(&self) -> Vec<Suit> { | |
186 | self.cards | |
187 | .iter() | |
188 | .map(|c| c.suit().expect("I shouldn't have jokers in my hand")) | |
189 | .collect() | |
190 | } | |
2d3998b9 SW |
191 | /// Make a new Hand that contains only cards of the requested suit |
192 | fn filter_by_suit(&self, suit: Suit) -> Self { | |
83741fed SW |
193 | self.filter_by_suits(&[suit]) |
194 | } | |
195 | /// Make a new Hand that contains only cards of the requested suits | |
196 | fn filter_by_suits(&self, suits: &[Suit]) -> Self { | |
2d3998b9 SW |
197 | Self { |
198 | cards: self | |
199 | .cards | |
200 | .iter() | |
83741fed | 201 | .filter(|c| suits.contains(&c.suit().expect("I shouldn't have jokers in my hand"))) |
2d3998b9 SW |
202 | .copied() |
203 | .collect(), | |
204 | } | |
205 | } | |
b6963095 SW |
206 | } |
207 | ||
208 | #[derive(Copy, Clone)] | |
209 | pub struct PlayerIndex(usize); | |
210 | impl PlayerIndex { | |
211 | fn next(self, num_players: usize) -> Self { | |
212 | Self((self.0 + 1) % num_players) | |
213 | } | |
214 | } | |
215 | ||
216 | #[derive(Copy, Clone, Debug)] | |
217 | pub enum Play { | |
218 | Play(Card), | |
219 | Draw, | |
220 | } | |
221 | ||
222 | #[derive(Eq, PartialEq)] | |
223 | pub enum Phase { | |
224 | Play, | |
225 | Momentum, | |
226 | } | |
227 | ||
cc2b69f3 | 228 | #[derive(Debug)] |
b6963095 SW |
229 | pub enum GameOutcome { |
230 | Loss, | |
231 | Win, | |
232 | } | |
233 | ||
234 | pub enum PlayOutcome { | |
235 | Continue, | |
236 | End(GameOutcome), | |
237 | } | |
238 | ||
239 | pub struct Game { | |
240 | mad_science_tokens: i8, | |
241 | progress: [i8; NUM_SUITS], | |
242 | path_lengths: [PathLength; NUM_SUITS], | |
243 | path_length_info: [PathLengthInfo; NUM_SUITS], | |
244 | library: Library, | |
245 | discard: Discard, | |
246 | hands: Vec<Hand>, | |
247 | turn: PlayerIndex, | |
248 | phase: Phase, | |
249 | } | |
250 | impl Game { | |
251 | pub fn add_player(&mut self) { | |
252 | self.hands.push(Hand::default()); | |
253 | for _ in 0..STARTING_CARDS { | |
254 | self.draw_for_player(PlayerIndex(self.hands.len() - 1)); | |
255 | } | |
256 | } | |
257 | /// # Errors | |
258 | /// | |
259 | /// Will return `Err` on invalid plays, like trying to draw during Play phase, | |
260 | /// or trying to play a card that's not in your hand. | |
261 | pub fn play(&mut self, play: Play) -> Result<PlayOutcome, &'static str> { | |
262 | match play { | |
263 | Play::Play(card) => self.play_card(card), | |
264 | Play::Draw => self.draw_for_momentum(), | |
265 | } | |
266 | } | |
267 | ||
268 | #[must_use] | |
269 | pub fn current_player_hand(&self) -> &Hand { | |
270 | &self.hands[self.turn.0] | |
271 | } | |
83741fed SW |
272 | #[must_use] |
273 | pub fn next_player_hand(&self) -> &Hand { | |
274 | &self.hands[self.turn.next(self.hands.len()).0] | |
275 | } | |
276 | #[must_use] | |
b6963095 SW |
277 | fn player_hand_mut(&mut self, pi: PlayerIndex) -> &mut Hand { |
278 | &mut self.hands[pi.0] | |
279 | } | |
83741fed | 280 | #[must_use] |
b6963095 SW |
281 | fn current_player_hand_mut(&mut self) -> &mut Hand { |
282 | self.player_hand_mut(self.turn) | |
283 | } | |
284 | ||
285 | fn play_card(&mut self, card: Card) -> Result<PlayOutcome, &'static str> { | |
286 | let momentum = self.apply_card(card)?; | |
287 | if self.phase == Phase::Play && momentum { | |
288 | self.phase = Phase::Momentum; | |
289 | Ok(PlayOutcome::Continue) | |
290 | } else { | |
291 | Ok(self.end_of_turn()) | |
292 | } | |
293 | } | |
294 | fn draw_for_momentum(&mut self) -> Result<PlayOutcome, &'static str> { | |
295 | if self.phase != Phase::Momentum { | |
296 | return Err("You don't have momentum"); | |
297 | } | |
298 | self.draw_for_player(self.turn); | |
299 | Ok(self.end_of_turn()) | |
300 | } | |
301 | ||
302 | fn draw_for_player(&mut self, pi: PlayerIndex) { | |
303 | loop { | |
304 | if let Some(card) = self.library.draw(&mut self.discard) { | |
305 | if card.is_joker() { | |
306 | self.remove_mad_science_token(); | |
307 | self.discard.discard(card); | |
308 | } else { | |
309 | self.player_hand_mut(pi).add(card); | |
310 | break; | |
311 | } | |
312 | } else { | |
313 | println!("Library ran out of cards"); | |
314 | } | |
315 | } | |
316 | } | |
317 | fn remove_mad_science_token(&mut self) { | |
318 | loop { | |
319 | self.mad_science_tokens -= 1; | |
320 | if self.mad_science_tokens != 0 { | |
321 | break; | |
322 | } | |
323 | } | |
324 | } | |
325 | fn make_progress(&mut self, card: Card) { | |
326 | let rank = card.rank().expect("Can't play jokers").0; | |
327 | if rank < 6 { | |
328 | let roll = rand::thread_rng().gen_range(1..=6); | |
329 | if roll > rank { | |
330 | self.remove_mad_science_token(); | |
331 | } | |
332 | } | |
333 | self.progress[usize::from(card.suit().expect("Can't play jokers").0)] += 1; | |
334 | } | |
335 | fn forecast(&mut self, card: Card) { | |
336 | let suit = usize::from(card.suit().expect("Can't play jokers").0); | |
337 | self.path_length_info[suit].reveal_random(self.path_lengths[suit]); | |
338 | } | |
339 | // Returns whether or not this play grants momentum | |
340 | fn apply_card(&mut self, card: Card) -> Result<bool, &'static str> { | |
341 | self.current_player_hand_mut().remove(card)?; | |
342 | if card.rank().expect("Can't play jokers").is_face() { | |
343 | self.forecast(card); | |
344 | } else { | |
345 | self.make_progress(card); | |
346 | } | |
347 | let suits_match = self | |
348 | .discard | |
349 | .top() | |
350 | .map_or(false, |dis| dis.suit() == card.suit()); | |
351 | self.discard.discard(card); | |
352 | Ok(suits_match) | |
353 | } | |
354 | fn valid(&self) -> bool { | |
355 | 108 == (self.library.len() | |
356 | + self.discard.len() | |
357 | + self.hands.iter().map(Hand::len).sum::<usize>()) | |
358 | } | |
359 | fn roll_mad_science(&mut self) -> PlayOutcome { | |
360 | let mut tokens = std::iter::from_fn(|| Some(rand::thread_rng().gen_bool(0.5))) | |
361 | .take(usize::try_from(self.mad_science_tokens.abs()).expect("wat?")); | |
362 | let keep_going = if self.mad_science_tokens > 0 { | |
363 | tokens.any(|t| !t) | |
364 | } else { | |
365 | tokens.all(|t| !t) | |
366 | }; | |
367 | if keep_going { | |
368 | PlayOutcome::Continue | |
369 | } else { | |
370 | PlayOutcome::End(self.final_score()) | |
371 | } | |
372 | } | |
373 | fn final_score(&self) -> GameOutcome { | |
374 | if self | |
375 | .progress | |
376 | .iter() | |
377 | .zip(self.path_lengths.iter()) | |
7259d43b | 378 | .any(|(&prog, len)| prog >= len.0.value().try_into().expect("wat?")) |
b6963095 SW |
379 | { |
380 | GameOutcome::Win | |
381 | } else { | |
382 | GameOutcome::Loss | |
383 | } | |
384 | } | |
385 | fn end_of_turn(&mut self) -> PlayOutcome { | |
386 | assert!(self.valid()); | |
387 | self.phase = Phase::Play; | |
388 | self.turn = self.turn.next(self.hands.len()); | |
389 | if self.turn.0 == 0 { | |
390 | if let PlayOutcome::End(game_outcome) = self.roll_mad_science() { | |
391 | return PlayOutcome::End(game_outcome); | |
392 | } | |
393 | } | |
394 | self.draw_for_player(self.turn); | |
395 | assert!(self.valid()); | |
396 | PlayOutcome::Continue | |
397 | } | |
398 | } | |
399 | impl Default for Game { | |
400 | fn default() -> Self { | |
401 | Self { | |
402 | mad_science_tokens: STARTING_MAD_SCIENCE_TOKENS, | |
403 | progress: [STARTING_PROGRESS; NUM_SUITS], | |
404 | path_lengths: std::iter::from_fn(|| Some(PathLength::random())) | |
405 | .take(NUM_SUITS) | |
406 | .collect::<Vec<_>>() | |
407 | .try_into() | |
408 | .expect("wat?"), | |
409 | path_length_info: [PathLengthInfo::default(); NUM_SUITS], | |
410 | library: Library::new(shuffled( | |
411 | [ | |
412 | deck(WithOrWithoutJokers::WithJokers), | |
413 | deck(WithOrWithoutJokers::WithJokers), | |
414 | ] | |
415 | .concat(), | |
416 | )), | |
417 | discard: Discard::default(), | |
418 | hands: vec![], | |
419 | turn: PlayerIndex(0), | |
420 | phase: Phase::Play, | |
421 | } | |
422 | } | |
423 | } | |
424 | ||
425 | pub struct Player(Box<dyn FnMut(&Game) -> Play>); | |
cc2b69f3 SW |
426 | impl Player { |
427 | #[must_use] | |
428 | pub fn new<T>(f: T) -> Self | |
429 | where | |
430 | T: FnMut(&Game) -> Play + 'static, | |
431 | { | |
432 | Self(Box::new(f)) | |
433 | } | |
434 | } | |
b6963095 | 435 | |
9cf05d1a SW |
436 | #[must_use] |
437 | pub fn random_player(draw_chance: f64) -> Player { | |
438 | Player(Box::new(move |game: &Game| -> Play { | |
aa0622ab SW |
439 | match game.phase { |
440 | Phase::Play => Play::Play( | |
441 | *game | |
442 | .current_player_hand() | |
443 | .random() | |
444 | .expect("I always have a card to play because I just drew one"), | |
445 | ), | |
446 | Phase::Momentum => { | |
447 | if rand::thread_rng().gen_bool(draw_chance) { | |
448 | Play::Draw | |
449 | } else { | |
450 | match game.current_player_hand().random() { | |
451 | Some(card) => Play::Play(*card), | |
452 | None => Play::Draw, | |
453 | } | |
b6963095 SW |
454 | } |
455 | } | |
456 | } | |
9cf05d1a | 457 | })) |
b6963095 SW |
458 | } |
459 | ||
a3618af6 | 460 | /// When available, make plays that grant momentum. |
9cf05d1a | 461 | #[must_use] |
a3618af6 | 462 | pub fn momentum_player(mut fallback: Player) -> Player { |
9cf05d1a | 463 | Player(Box::new(move |game: &Game| -> Play { |
c5e4b1eb SW |
464 | if game.phase == Phase::Play { |
465 | if let Some(suit) = game.discard.top().and_then(Card::suit) { | |
466 | if let Some(card) = game.current_player_hand().filter_by_suit(suit).random() { | |
467 | return Play::Play(*card); | |
2d3998b9 SW |
468 | } |
469 | } | |
2d3998b9 | 470 | } |
c5e4b1eb | 471 | fallback.0(game) |
9cf05d1a | 472 | })) |
2d3998b9 SW |
473 | } |
474 | ||
83741fed SW |
475 | /// Try to coordinate to give the next player a momentum opportunity. |
476 | #[must_use] | |
477 | pub fn coordinating_player(mut fallback: Player) -> Player { | |
478 | Player(Box::new(move |game: &Game| -> Play { | |
479 | if let Some(card) = game | |
480 | .current_player_hand() | |
481 | .filter_by_suits(&game.next_player_hand().suits()) | |
482 | .random() | |
483 | { | |
484 | return Play::Play(*card); | |
485 | } | |
486 | fallback.0(game) | |
487 | })) | |
488 | } | |
489 | ||
b6963095 SW |
490 | /// # Errors |
491 | /// | |
492 | /// Will return `Err` on invalid plays, like trying to draw during Play phase, | |
493 | /// or trying to play a card that's not in your hand. | |
494 | pub fn play(mut game: Game, mut players: Vec<Player>) -> Result<GameOutcome, &'static str> { | |
495 | game.draw_for_player(game.turn); | |
496 | loop { | |
497 | if let PlayOutcome::End(game_outcome) = game.play(players[game.turn.0].0(&game))? { | |
498 | return Ok(game_outcome); | |
499 | } | |
500 | } | |
b255b7c6 SW |
501 | } |
502 | ||
754e9730 SW |
503 | #[cfg(test)] |
504 | mod tests { | |
505 | use super::*; | |
506 | ||
507 | #[test] | |
57f490a0 SW |
508 | fn path_length_info_random_reveal() { |
509 | let length = PathLength(Rank(7)); | |
510 | let mut pli = PathLengthInfo::default(); | |
511 | for _ in 0..12 { | |
512 | let old_pli = PathLengthInfo::clone(&pli); | |
513 | match pli.reveal_random(length) { | |
514 | None => panic!("Nothing revealed?"), | |
515 | Some(r) => { | |
516 | assert!(!old_pli.is_showing(r)); | |
517 | assert!(pli.is_showing(r)); | |
518 | } | |
519 | } | |
520 | assert_eq!(pli.0.count_ones(), 1 + old_pli.0.count_ones()); | |
521 | } | |
522 | assert!(pli.reveal_random(length).is_none()); | |
754e9730 | 523 | } |
09822a98 SW |
524 | |
525 | #[test] | |
526 | fn test_deck() { | |
10e7da7b | 527 | use WithOrWithoutJokers::*; |
b8e315ac SW |
528 | let d = deck(WithoutJokers); |
529 | let rank_sum: u32 = d | |
530 | .iter() | |
531 | .map(Card::rank) | |
532 | .flatten() | |
533 | .map(|r| u32::from(r.value())) | |
534 | .sum(); | |
535 | assert_eq!(rank_sum, 364); | |
10e7da7b | 536 | let _dj = deck(WithJokers); |
09822a98 | 537 | } |
644e6c7a SW |
538 | |
539 | #[test] | |
540 | fn test_library() { | |
541 | let mut lib = Library::new(vec![Card(7)]); | |
542 | let mut dis = Discard::default(); | |
543 | dis.discard(Card(8)); | |
544 | dis.discard(Card(9)); | |
545 | assert_eq!(lib.draw(&mut dis), Some(Card(7))); | |
546 | assert_eq!(lib.draw(&mut dis), Some(Card(8))); | |
547 | assert_eq!(lib.draw(&mut dis), None); | |
548 | } | |
b255b7c6 SW |
549 | |
550 | #[test] | |
551 | fn test_hand() { | |
552 | let mut h = Hand::default(); | |
553 | assert!(h.remove(Card(4)).is_err()); | |
554 | h.add(Card(4)); | |
555 | assert!(h.remove(Card(3)).is_err()); | |
556 | assert!(h.remove(Card(4)).is_ok()); | |
557 | assert!(h.remove(Card(4)).is_err()); | |
558 | } | |
b6963095 SW |
559 | |
560 | #[test] | |
561 | fn test_game() { | |
7d0aa2d6 | 562 | for num_players in 1..10 { |
83741fed SW |
563 | let players: Vec<_> = std::iter::from_fn(|| { |
564 | Some(momentum_player(coordinating_player(random_player(0.5)))) | |
565 | }) | |
566 | .take(num_players) | |
567 | .collect(); | |
7d0aa2d6 SW |
568 | let mut game = Game::default(); |
569 | for _ in 0..num_players { | |
570 | game.add_player(); | |
571 | } | |
572 | assert!(play(game, players).is_ok()); | |
573 | } | |
b6963095 | 574 | } |
754e9730 | 575 | } |