]> git.scottworley.com Git - pluta-lesnura/blob - src/lib.rs
Refactor momentum_player() for clarity
[pluta-lesnura] / src / lib.rs
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 if game.phase == Phase::Play {
448 if let Some(suit) = game.discard.top().and_then(Card::suit) {
449 if let Some(card) = game.current_player_hand().filter_by_suit(suit).random() {
450 return Play::Play(*card);
451 }
452 }
453 }
454 fallback.0(game)
455 }))
456 }
457
458 /// # Errors
459 ///
460 /// Will return `Err` on invalid plays, like trying to draw during Play phase,
461 /// or trying to play a card that's not in your hand.
462 pub fn play(mut game: Game, mut players: Vec<Player>) -> Result<GameOutcome, &'static str> {
463 game.draw_for_player(game.turn);
464 loop {
465 if let PlayOutcome::End(game_outcome) = game.play(players[game.turn.0].0(&game))? {
466 return Ok(game_outcome);
467 }
468 }
469 }
470
471 #[cfg(test)]
472 mod tests {
473 use super::*;
474
475 #[test]
476 fn path_length_info_random_reveal() {
477 let length = PathLength(Rank(7));
478 let mut pli = PathLengthInfo::default();
479 for _ in 0..12 {
480 let old_pli = PathLengthInfo::clone(&pli);
481 match pli.reveal_random(length) {
482 None => panic!("Nothing revealed?"),
483 Some(r) => {
484 assert!(!old_pli.is_showing(r));
485 assert!(pli.is_showing(r));
486 }
487 }
488 assert_eq!(pli.0.count_ones(), 1 + old_pli.0.count_ones());
489 }
490 assert!(pli.reveal_random(length).is_none());
491 }
492
493 #[test]
494 fn test_deck() {
495 use WithOrWithoutJokers::*;
496 let d = deck(WithoutJokers);
497 let rank_sum: u32 = d
498 .iter()
499 .map(Card::rank)
500 .flatten()
501 .map(|r| u32::from(r.value()))
502 .sum();
503 assert_eq!(rank_sum, 364);
504 let _dj = deck(WithJokers);
505 }
506
507 #[test]
508 fn test_library() {
509 let mut lib = Library::new(vec![Card(7)]);
510 let mut dis = Discard::default();
511 dis.discard(Card(8));
512 dis.discard(Card(9));
513 assert_eq!(lib.draw(&mut dis), Some(Card(7)));
514 assert_eq!(lib.draw(&mut dis), Some(Card(8)));
515 assert_eq!(lib.draw(&mut dis), None);
516 }
517
518 #[test]
519 fn test_hand() {
520 let mut h = Hand::default();
521 assert!(h.remove(Card(4)).is_err());
522 h.add(Card(4));
523 assert!(h.remove(Card(3)).is_err());
524 assert!(h.remove(Card(4)).is_ok());
525 assert!(h.remove(Card(4)).is_err());
526 }
527
528 #[test]
529 fn test_game() {
530 for num_players in 1..10 {
531 let players: Vec<_> = std::iter::from_fn(|| Some(momentum_player(random_player(0.5))))
532 .take(num_players)
533 .collect();
534 let mut game = Game::default();
535 for _ in 0..num_players {
536 game.add_player();
537 }
538 assert!(play(game, players).is_ok());
539 }
540 }
541 }