hands: HashMap<Username, HashSet<Card>>,
turn_up: Option<Card>,
box_cards: HashSet<Card>,
+ revealed: HashSet<Card>,
pegging_cards: Vec<(Username, Card)>,
used_pegging_cards: Vec<(Username, Card)>,
players_still_in: HashSet<Username>,
hands: HashMap::new(),
turn_up: None,
box_cards: HashSet::new(),
+ revealed: HashSet::new(),
pegging_cards: Vec::new(),
used_pegging_cards: Vec::new(),
players_still_in: HashSet::new(),
}
}
+ fn unrevealed_card(&self, username: Username) -> Option<Card> {
+ self.hands.get(&username).and_then(|hand| hand.iter().filter(|card| !self.revealed.contains(*card)).next().copied())
+ }
+
fn four_card_hand(&self, username: Username) -> Option<[Card; 4]> {
self.hands.get(&username).and_then(|hand| {
let hand: Vec<_> = hand.into_iter().collect();
}
fn player_has_won(&self, username: Username) -> bool {
- self.points.get(&username).map_or(false, |points| *points >= 121)
+ self.points.get(&username).map_or(false, |points| *points >= self.settings.target_score)
}
fn winner(&self) -> Option<Username> {
- self.points.iter().filter(|&(_, points)| *points >= 121).next().map(|(username, _)| *username)
+ self.points.iter().filter(|&(_, points)| *points >= self.settings.target_score).next().map(|(username, _)| *username)
}
}
self.hands.clear();
self.receiver = self.seats.player_after(username);
self.box_cards.clear();
+ self.revealed.clear();
self.pegging_cards.clear();
self.players_still_in.clear();
self.active = None;
if self.last_pegging_score().is_some() {
self.state = State::ScoringPegging;
}
+ self.active = self.next_player_still_in();
Ok(())
}
(State::Pegging, Action::Pass) => {
self.state = State::ScoringPegging;
self.used_pegging_cards.extend(self.pegging_cards.drain(..));
self.players_still_in = self.hands.iter().filter(|(_, cards)| !cards.is_empty()).map(|(&username, _)| username).collect();
- self.active = self.next_player_still_in();
},
active => self.active = active,
}
}
(State::ScoringPegging, Action::Score { points, .. }) => {
*self.points.entry(username).or_default() += points as u32;
- match self.next_player_still_in() {
- None => {
- self.used_pegging_cards.extend(self.pegging_cards.drain(..));
- self.players_still_in = self.hands.iter().filter(|(_, cards)| !cards.is_empty()).map(|(&username, _)| username).collect();
+ if self.next_player_still_in().is_none() {
+ self.used_pegging_cards.extend(self.pegging_cards.drain(..));
+ self.players_still_in = self.hands.iter().filter(|(_, cards)| !cards.is_empty()).map(|(&username, _)| username).collect();
+ self.active = Some(username);
+ if !self.players_still_in.contains(&username) {
self.active = self.next_player_still_in();
}
- active => self.active = active,
}
if self.player_has_won(username) {
self.state = State::ScoringPegging;
}
Ok(())
}
+ (State::Scoring, Action::RevealCard { card }) => {
+ self.revealed.insert(card);
+ Ok(())
+ }
(State::Scoring, Action::Score { points, .. }) => {
*self.points.entry(username).or_default() += points as u32;
if self.dealer == Some(username) {
}
Ok(())
}
+ (State::ScoringBox, Action::RevealCard { card }) => {
+ self.revealed.insert(card);
+ Ok(())
+ }
(State::ScoringBox, Action::Score { points, .. }) => {
*self.points.entry(username).or_default() += points as u32;
self.active = None;
if let Some(username) = self.winner() {
DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::WinGame }))
} else if let Some(username) = self.active {
- if let (Some(hand), Some(turn_up)) = (self.four_card_hand(username), self.turn_up) {
+ if let Some(card) = self.unrevealed_card(username) {
+ DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::RevealCard { card } }))
+ } else if let (Some(hand), Some(turn_up)) = (self.four_card_hand(username), self.turn_up) {
let score = score_4_card_cribbage_hand(hand, turn_up, self.state == State::ScoringBox);
DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::Score { points: score.points() as u64, reason: format!("{}", score) } }))
} else {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn test_game(actions: Vec<UserAction>, settings: CribbageSettings, seed: Seed) {
+ let mut game = Cribbage::new(0, settings, seed);
+ for action in actions {
+ match action.action {
+ Action::Join { .. } | Action::PutInBox { .. } | Action::PlayCard { .. } => {
+ let validated = game.validate_action(action.clone()).unwrap();
+ assert_eq!(ValidatedUserAction(action), validated);
+ game.take_action(validated).unwrap();
+ }
+ _ => {
+ let dealer_action = game.next_dealer_action(action.timestamp);
+ if let DealerAction::TakeAction(ValidatedUserAction(dealer_action)) = dealer_action {
+ assert_eq!(action, dealer_action);
+ game.take_action(ValidatedUserAction(action)).unwrap();
+ } else {
+ panic!("Expected DealerAction::TakeAction{{ {:?} }}, got {:?}", action, dealer_action);
+ }
+ }
+ }
+ }
+ }
+
+ #[test]
+ fn simple_cribbage_game() {
+ let actions = r#"[
+ {"timestamp":1685220615999,"username":"Geoff","action":{"action":"Join","seat":0,"chips":0}},
+ {"timestamp":1685220772960,"username":"Aga","action":{"action":"Join","seat":1,"chips":0}},
+ {"timestamp":1685308376245,"username":"Geoff","action":{"action":"NextToDeal"}},
+ {"timestamp":1685308376245,"username":"Aga","action":{"action":"ReceiveCard","card":{"rank":"Three","suit":"Hearts"}}},
+ {"timestamp":1685308376246,"username":"Geoff","action":{"action":"ReceiveCard","card":{"rank":"Three","suit":"Clubs"}}},
+ {"timestamp":1685308376246,"username":"Aga","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Hearts"}}},
+ {"timestamp":1685308376246,"username":"Geoff","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Clubs"}}},
+ {"timestamp":1685308376246,"username":"Aga","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Clubs"}}},
+ {"timestamp":1685308376246,"username":"Geoff","action":{"action":"ReceiveCard","card":{"rank":"Two","suit":"Spades"}}},
+ {"timestamp":1685308376247,"username":"Aga","action":{"action":"ReceiveCard","card":{"rank":"Jack","suit":"Diamonds"}}},
+ {"timestamp":1685308376247,"username":"Geoff","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Clubs"}}},
+ {"timestamp":1685308376247,"username":"Aga","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Diamonds"}}},
+ {"timestamp":1685308376247,"username":"Geoff","action":{"action":"ReceiveCard","card":{"rank":"Seven","suit":"Clubs"}}},
+ {"timestamp":1685308376248,"username":"Aga","action":{"action":"ReceiveCard","card":{"rank":"Ten","suit":"Clubs"}}},
+ {"timestamp":1685308376248,"username":"Geoff","action":{"action":"ReceiveCard","card":{"rank":"Five","suit":"Clubs"}}},
+ {"timestamp":1685308376248,"username":"Geoff","action":{"action":"EndDeal"}},
+ {"timestamp":1685308376248,"username":"Geoff","action":{"action":"PutInBox","card":{"rank":"Five","suit":"Clubs"}}},
+ {"timestamp":1685308376248,"username":"Geoff","action":{"action":"PutInBox","card":{"rank":"King","suit":"Clubs"}}},
+ {"timestamp":1685308376248,"username":"Aga","action":{"action":"PutInBox","card":{"rank":"Ten","suit":"Clubs"}}},
+ {"timestamp":1685308376248,"username":"Aga","action":{"action":"PutInBox","card":{"rank":"Jack","suit":"Diamonds"}}},
+ {"timestamp":1685308750725,"username":"Geoff","action":{"action":"CommunityCard","card":{"rank":"Six","suit":"Diamonds"}}},
+ {"timestamp":1685308376248,"username":"Aga","action":{"action":"PlayCard","card":{"rank":"King","suit":"Diamonds"}}},
+ {"timestamp":1685308376248,"username":"Geoff","action":{"action":"PlayCard","card":{"rank":"Two","suit":"Spades"}}},
+ {"timestamp":1685308376248,"username":"Aga","action":{"action":"PlayCard","card":{"rank":"Three","suit":"Hearts"}}},
+ {"timestamp":1685309372399,"username":"Aga","action":{"action":"Score","points":2,"reason":"Fifteen for two"}}
+ ]"#;
+
+ let actions = serde_json::from_str(actions).unwrap();
+
+ let settings = r#"{"format":"Cribbage","title":"Cribbage Testing","max_players":2,"target_score":181,"start_time":null}"#;
+ let settings = serde_json::from_str(settings).unwrap();
+
+ let seed = r#"{"rng":"ChaCha20","seed":"3789582e9d1a9229bd22d2a61b156bcf907e4cb44d613f97c08b16ec73ff0d90"}"#;
+ let seed = serde_json::from_str(seed).unwrap();
+
+ test_game(actions, settings, seed);
+ }
+}