]> git.scottworley.com Git - pluta-lesnura/blame - src/lib.rs
Coordinate to grant momentum to the next player
[pluta-lesnura] / src / lib.rs
CommitLineData
b6963095 1use rand::seq::SliceRandom;
57f490a0
SW
2use rand::Rng;
3
95b540e1
SW
4pub const NUM_RANKS: usize = 13;
5pub const NUM_SUITS: usize = 4;
6pub const NUM_JOKERS: usize = 2;
7pub const NUM_CARDS: usize = NUM_RANKS * NUM_SUITS + NUM_JOKERS;
57f490a0 8
b6963095
SW
9pub const STARTING_CARDS: u8 = 3;
10pub const STARTING_MAD_SCIENCE_TOKENS: i8 = 15;
11pub const STARTING_PROGRESS: i8 = -10;
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57f490a0 14pub struct Rank(u8);
b8e315ac
SW
15impl 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)]
36pub struct Suit(u8);
37
644e6c7a 38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
09822a98
SW
39pub struct Card(u8);
40impl 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)]
56pub enum WithOrWithoutJokers {
57 WithJokers,
58 WithoutJokers,
09822a98
SW
59}
60
61#[must_use]
10e7da7b 62pub 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
71fn shuffle(cards: &mut Vec<Card>) {
72 cards.shuffle(&mut rand::thread_rng());
73}
74#[must_use]
75fn shuffled(mut cards: Vec<Card>) -> Vec<Card> {
76 shuffle(&mut cards);
77 cards
78}
79
80#[derive(Clone, Copy, Debug)]
57f490a0 81pub struct PathLength(Rank);
b6963095
SW
82impl PathLength {
83 #[must_use]
84 pub fn random() -> Self {
85 Self(Rank::random())
86 }
87}
57f490a0
SW
88
89#[derive(Clone, Copy, Default)]
90pub struct PathLengthInfo(u16);
91impl 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)]
122pub struct Discard {
123 cards: Vec<Card>,
124}
125impl 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
138pub struct Library {
139 cards: Vec<Card>,
140}
141impl 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)]
162pub struct Hand {
b255b7c6
SW
163 cards: Vec<Card>,
164}
b255b7c6
SW
165impl 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)]
209pub struct PlayerIndex(usize);
210impl PlayerIndex {
211 fn next(self, num_players: usize) -> Self {
212 Self((self.0 + 1) % num_players)
213 }
214}
215
216#[derive(Copy, Clone, Debug)]
217pub enum Play {
218 Play(Card),
219 Draw,
220}
221
222#[derive(Eq, PartialEq)]
223pub enum Phase {
224 Play,
225 Momentum,
226}
227
cc2b69f3 228#[derive(Debug)]
b6963095
SW
229pub enum GameOutcome {
230 Loss,
231 Win,
232}
233
234pub enum PlayOutcome {
235 Continue,
236 End(GameOutcome),
237}
238
239pub 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}
250impl 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}
399impl 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
425pub struct Player(Box<dyn FnMut(&Game) -> Play>);
cc2b69f3
SW
426impl 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]
437pub 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 462pub 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]
477pub 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.
494pub 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)]
504mod 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}