From adaa513ff85e72c99e80c5fe7c844fc174ca5304 Mon Sep 17 00:00:00 2001 From: Jeff Wu Date: Sun, 13 Mar 2016 13:50:38 -0700 Subject: [PATCH] cheating strategy improvements --- src/game.rs | 87 +++++++++++++++++++------- src/main.rs | 20 +++--- src/simulator.rs | 2 + src/strategies/cheating.rs | 123 ++++++++++++++++++++++++++++--------- 4 files changed, 173 insertions(+), 59 deletions(-) diff --git a/src/game.rs b/src/game.rs index 13ccb3c..5e420cf 100644 --- a/src/game.rs +++ b/src/game.rs @@ -9,7 +9,7 @@ use info::*; */ pub type Color = &'static str; -pub const COLORS: [Color; 5] = ["blue", "red", "yellow", "white", "green"]; +pub const COLORS: [Color; 5] = ["red", "yellow", "green", "blue", "white"]; pub fn display_color(color: Color) -> char { color.chars().next().unwrap() } @@ -321,6 +321,7 @@ pub struct BoardState { pub turn: u32, // // whose turn is it? pub player: Player, + pub hand_size: u32, pub hints_total: u32, pub hints_remaining: u32, @@ -343,6 +344,7 @@ impl BoardState { fireworks: fireworks, discard: Discard::new(), num_players: opts.num_players, + hand_size: opts.hand_size, player: 0, turn: 1, hints_total: opts.num_hints, @@ -360,10 +362,13 @@ impl BoardState { } } + fn get_firework(&self, color: &Color) -> &Firework { + self.fireworks.get(color).unwrap() + } + // returns whether a card would place on a firework pub fn is_playable(&self, card: &Card) -> bool { - let firework = self.fireworks.get(card.color).unwrap(); - Some(card.value) == firework.desired_value() + Some(card.value) == self.get_firework(&card.color).desired_value() } pub fn was_played(&self, card: &Card) -> bool { @@ -399,7 +404,7 @@ impl BoardState { } // is never going to play, based on discard + fireworks - pub fn is_unplayable(&self, card: &Card) -> bool { + pub fn is_dead(&self, card: &Card) -> bool { let firework = self.fireworks.get(card.color).unwrap(); if firework.complete() { true @@ -413,20 +418,20 @@ impl BoardState { } } - // cannot be discarded without sacrificing score, based on discard + fireworks - pub fn is_undiscardable(&self, card: &Card) -> bool { + // can be discarded without necessarily sacrificing score, based on discard + fireworks + pub fn is_dispensable(&self, card: &Card) -> bool { let firework = self.fireworks.get(card.color).unwrap(); if firework.complete() { - false + true } else { let desired = firework.desired_value().unwrap(); if card.value < desired { - false + true } else { if card.value > self.highest_attainable(&card.color) { - false + true } else { - self.discard.remaining(&card) == 1 + self.discard.remaining(&card) != 1 } } } @@ -454,12 +459,23 @@ impl BoardState { pub fn player_to_right(&self, player: &Player) -> Player { (player - 1) % self.num_players } + + pub fn is_over(&self) -> bool { + (self.lives_remaining == 0) || (self.deckless_turns_remaining == 0) + } } impl fmt::Display for BoardState { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - try!(f.write_str(&format!( - "Turn {} (Player {}'s turn):\n", self.turn, self.player - ))); + if self.is_over() { + try!(f.write_str(&format!( + "Turn {} (GAME ENDED):\n", self.turn + ))); + } else { + try!(f.write_str(&format!( + "Turn {} (Player {}'s turn):\n", self.turn, self.player + ))); + } + let deck_size = self.deck_size(); try!(f.write_str(&format!( "{} cards remaining in deck\n", deck_size @@ -476,8 +492,8 @@ impl fmt::Display for BoardState { "{}/{} lives remaining\n", self.lives_remaining, self.lives_total ))); try!(f.write_str("Fireworks:\n")); - for (_, firework) in &self.fireworks { - try!(f.write_str(&format!(" {}\n", firework))); + for color in COLORS.iter() { + try!(f.write_str(&format!(" {}\n", self.get_firework(color)))); } try!(f.write_str("Discard:\n")); try!(f.write_str(&format!("{}\n", self.discard))); @@ -499,6 +515,26 @@ pub struct GameStateView<'a> { // board state pub board: &'a BoardState, } +impl <'a> GameStateView<'a> { + pub fn has_card(&self, player: &Player, card: &Card) -> bool { + assert!(self.player != *player, "Cannot query about your own cards!"); + let state = self.other_player_states.get(player).unwrap(); + for other_card in &state.hand { + if *card == *other_card { + return true; + } + } + false + } + pub fn can_see(&self, card: &Card) -> bool { + for other_player in self.other_player_states.keys() { + if self.has_card(other_player, card) { + return true; + } + } + false + } +} // complete game state (known to nobody!) #[derive(Debug)] @@ -511,7 +547,7 @@ impl fmt::Display for GameState { try!(f.write_str("==========================\n")); try!(f.write_str("Hands:\n")); try!(f.write_str("==========================\n")); - for player in 0..self.board.num_players { + for player in self.board.get_players() { let state = &self.player_states.get(&player).unwrap(); try!(f.write_str(&format!("player {} {}\n", player, state))); } @@ -552,8 +588,7 @@ impl GameState { pub fn is_over(&self) -> bool { // TODO: add condition that fireworks cannot be further completed? - (self.board.lives_remaining == 0) || - (self.board.deckless_turns_remaining == 0) + self.board.is_over() } pub fn score(&self) -> Score { @@ -580,13 +615,19 @@ impl GameState { fn take_from_hand(&mut self, index: usize) -> Card { let ref mut state = self.player_states.get_mut(&self.board.player).unwrap(); let (card, _) = state.take(index); - if let Some(new_card) = self.board.deck.pop() { - debug!("Drew new card, {}", new_card); - state.place(new_card); - } card } + fn replenish_hand(&mut self) { + let ref mut state = self.player_states.get_mut(&self.board.player).unwrap(); + if (state.hand.len() as u32) < self.board.hand_size { + if let Some(new_card) = self.board.deck.pop() { + debug!("Drew new card, {}", new_card); + state.place(new_card); + } + } + } + pub fn process_choice(&mut self, choice: &TurnChoice) { debug!("Player {}'s move", self.board.player); match choice { @@ -642,6 +683,8 @@ impl GameState { } } + self.replenish_hand(); + if self.board.deck.len() == 0 { self.board.deckless_turns_remaining -= 1; } diff --git a/src/main.rs b/src/main.rs index 621203f..101d447 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,9 +27,11 @@ impl log::Log for SimpleLogger { } fn main() { + // TODO: make a binary with command line options + log::set_logger(|max_log_level| { - max_log_level.set(log::LogLevelFilter::Trace); - // max_log_level.set(log::LogLevelFilter::Info); + // max_log_level.set(log::LogLevelFilter::Trace); + max_log_level.set(log::LogLevelFilter::Info); Box::new(SimpleLogger) }).unwrap(); @@ -50,13 +52,13 @@ fn main() { // }, // n // ); - // simulator::simulate_symmetric( - // &opts, - // strategies::cheating::CheatingStrategyConfig::new(), - // n - // ); - simulator::simulate_symmetric_once( - &opts, Some(993), + simulator::simulate_symmetric( + &opts, strategies::cheating::CheatingStrategyConfig::new(), + n ); + // simulator::simulate_symmetric_once( + // &opts, Some(999), + // strategies::cheating::CheatingStrategyConfig::new(), + // ); } diff --git a/src/simulator.rs b/src/simulator.rs index 93442c4..34b4263 100644 --- a/src/simulator.rs +++ b/src/simulator.rs @@ -38,6 +38,8 @@ pub fn simulate_once<'a>( i += 1; } + debug!("Initial state:\n{}", game); + while !game.is_over() { debug!("Turn {}", game.board.turn); let player = game.board.player; diff --git a/src/strategies/cheating.rs b/src/strategies/cheating.rs index f0821e1..a1c3377 100644 --- a/src/strategies/cheating.rs +++ b/src/strategies/cheating.rs @@ -5,11 +5,18 @@ use std::collections::HashMap; use simulator::*; use game::*; -// strategy that cheats by using Rc/RefCell +// strategy that explicitly cheats by using Rc/RefCell +// serves as a reference point for other strategies +// // Plays according to the following rules: // - if any card is playable, // play the card with the lowest value // - if a card is dead, discard it +// - if another player has same card in hand, discard it +// - if a card is discardable, discard it +// - if a hint exists, hint +// - discard the first card + #[allow(dead_code)] #[derive(Clone)] pub struct CheatingStrategyConfig { @@ -43,6 +50,7 @@ impl CheatingStrategy { next, view.other_player_states.get(&next).unwrap().hand.clone() ); } + // give a throwaway hint - we only do this when we have nothing to do fn throwaway_hint(&self, view: &GameStateView) -> TurnChoice { TurnChoice::Hint(Hint { @@ -50,11 +58,54 @@ impl CheatingStrategy { hinted: Hinted::Value(1) }) } + + // given a hand of cards, represents how badly it will need to play things + fn hand_play_value(&self, view: &GameStateView, hand: &Cards/*, all_viewable: HashMap> */) -> u32 { + // dead = 0 points + // indispensible = 5 + (5 - value) points + // playable, not in another hand = 2 point + // playable = 1 point + let mut value = 0; + for card in hand { + if view.board.is_dead(card) { + continue + } + if !view.board.is_dispensable(card) { + value += 10 - card.value; + } else { + value += 1; + } + } + value + } + + // how badly do we need to play a particular card + fn get_play_score(&self, view: &GameStateView, card: &Card) -> i32 { + let states = self.player_states_cheat.borrow(); + let my_hand = states.get(&self.me).unwrap(); + + let my_hand_value = self.hand_play_value(view, my_hand); + + for player in view.board.get_players() { + if player != self.me { + if view.has_card(&player, card) { + let their_hand_value = self.hand_play_value(view, states.get(&player).unwrap()); + // they can play this card, and have less urgent plays than i do + if their_hand_value <= my_hand_value { + return 1; + } + } + } + } + // there are no hints + // maybe value 5s more? + 5 + (5 - (card.value as i32)) + } } impl Strategy for CheatingStrategy { fn decide(&mut self, view: &GameStateView) -> TurnChoice { self.inform_next_player_cards(view); - if view.board.turn == 1 { + if view.board.turn <= view.board.num_players { // don't know my cards yet, just give a random hint return self.throwaway_hint(view); } @@ -66,41 +117,57 @@ impl Strategy for CheatingStrategy { }).peekable(); if playable_cards.peek() == None { - for card in my_cards { - if view.board.is_unplayable(card) { - let index = my_cards.iter().position(|iter_card| { - card == iter_card - }).unwrap(); - return TurnChoice::Discard(index); + // if anything is totally useless, discard it + for (i, card) in my_cards.iter().enumerate() { + if view.board.is_dead(card) { + return TurnChoice::Discard(i); } } - for card in my_cards { - if !view.board.is_undiscardable(card) { - let index = my_cards.iter().position(|iter_card| { - card == iter_card - }).unwrap(); - return TurnChoice::Discard(index); - } - } - // all my cards are undiscardable! if view.board.hints_remaining > 0 { return self.throwaway_hint(view); } - TurnChoice::Discard(0) - } else { - // play the lowest playable card - let mut play_card = playable_cards.next().unwrap(); - - let mut next_card_opt = playable_cards.next(); - while let Some(next_card) = next_card_opt { - if next_card.value < play_card.value { - play_card = next_card; + // All cards are plausibly useful. + // Play the best discardable card, according to the ordering induced by comparing + // (is in another hand, is dispensable, value) + // The higher, the better to discard + let mut discard_card = None; + let mut compval = (false, false, 0); + for card in my_cards { + let my_compval = ( + view.can_see(card), + view.board.is_dispensable(card), + card.value, + ); + if my_compval > compval { + discard_card = Some(card); + compval = my_compval; + } + } + if let Some(card) = discard_card { + let index = my_cards.iter().position(|iter_card| { + card == iter_card + }).unwrap(); + TurnChoice::Discard(index) + } else { + panic!("This shouldn't happen! No discardable card"); + } + } else { + // play the best playable card + // the higher the play_score, the better to play + let mut play_card = None; + let mut play_score = -1; + + while playable_cards.peek().is_some() { + let next_card = playable_cards.next().unwrap(); + let next_play_score = self.get_play_score(view, next_card); + if next_play_score > play_score { + play_card = Some(next_card); + play_score = next_play_score; } - next_card_opt = playable_cards.next(); } let index = my_cards.iter().position(|card| { - card == play_card + card == play_card.unwrap() }).unwrap(); TurnChoice::Play(index) }