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