Second attempt at outputting JSON for hanabi.live

We don't support any notes yet.

Inside our GameState, we annotate each card with its index in the
original (reverse) deck. In order to support per-card notes, we should
probably share those annotations with the players, and refactor the
player code around them.
This commit is contained in:
Felix Bauckholt 2019-02-24 21:52:03 +01:00
parent 7f384cc15d
commit 180d8c5f4e
10 changed files with 228 additions and 29 deletions

30
Cargo.lock generated
View file

@ -18,6 +18,11 @@ name = "getopts"
version = "0.2.14" version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "itoa"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.7" version = "0.2.7"
@ -49,6 +54,27 @@ dependencies = [
"getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", "getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ryu"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde"
version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde_json"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
"ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[metadata] [metadata]
@ -56,6 +82,10 @@ dependencies = [
"checksum float-ord 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e" "checksum float-ord 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e"
"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
"checksum getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9047cfbd08a437050b363d35ef160452c5fe8ea5187ae0a624708c91581d685" "checksum getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9047cfbd08a437050b363d35ef160452c5fe8ea5187ae0a624708c91581d685"
"checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b"
"checksum libc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "4870ef6725dde13394134e587e4ab4eca13cb92e916209a31c851b49131d3c75" "checksum libc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "4870ef6725dde13394134e587e4ab4eca13cb92e916209a31c851b49131d3c75"
"checksum log 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "038b5d13189a14e5b6ac384fdb7c691a45ef0885f6d2dddbf422e6c3506b8234" "checksum log 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "038b5d13189a14e5b6ac384fdb7c691a45ef0885f6d2dddbf422e6c3506b8234"
"checksum rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "2791d88c6defac799c3f20d74f094ca33b9332612d9aef9078519c82e4fe04a5" "checksum rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "2791d88c6defac799c3f20d74f094ca33b9332612d9aef9078519c82e4fe04a5"
"checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7"
"checksum serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)" = "9f301d728f2b94c9a7691c90f07b0b4e8a4517181d9461be94c04bddeb4bd850"
"checksum serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)" = "27dce848e7467aa0e2fcaf0a413641499c0b745452aaca1194d24dedde9e13c9"

View file

@ -10,3 +10,4 @@ getopts = "*"
fnv = "*" fnv = "*"
float-ord = "*" float-ord = "*"
crossbeam = "0.2.5" crossbeam = "0.2.5"
serde_json = "*"

View file

@ -389,7 +389,7 @@ impl BoardState {
} }
pub fn is_over(&self) -> bool { pub fn is_over(&self) -> bool {
(self.lives_remaining == 0) || (self.deckless_turns_remaining == 0) (self.lives_remaining == 0) || (self.deckless_turns_remaining == 0) || (self.score() == PERFECT_SCORE)
} }
} }
impl fmt::Display for BoardState { impl fmt::Display for BoardState {
@ -541,12 +541,28 @@ impl GameView for OwnedGameView {
} }
} }
// Internally, every card is annotated with its index in the deck in order to
// generate easy-to-interpret JSON output. These annotations are stripped off
// when passing GameViews to strategies.
//
// TODO: Maybe we should give strategies access to the annotations as well?
// This could simplify code like in InformationPlayerStrategy::update_public_info_for_discard_or_play.
// Also, this would let a strategy publish "notes" on cards more easily.
pub type AnnotatedCard = (usize, Card);
pub type AnnotatedCards = Vec<AnnotatedCard>;
fn strip_annotations(cards: &AnnotatedCards) -> Cards {
cards.iter().map(|(_i, card)| { card.clone() }).collect()
}
// complete game state (known to nobody!) // complete game state (known to nobody!)
#[derive(Debug)] #[derive(Debug)]
pub struct GameState { pub struct GameState {
pub hands: FnvHashMap<Player, Cards>, pub hands: FnvHashMap<Player, AnnotatedCards>,
// used to construct BorrowedGameViews
pub unannotated_hands: FnvHashMap<Player, Cards>,
pub board: BoardState, pub board: BoardState,
pub deck: Cards, pub deck: AnnotatedCards,
} }
impl fmt::Display for GameState { impl fmt::Display for GameState {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
@ -557,7 +573,7 @@ impl fmt::Display for GameState {
for player in self.board.get_players() { for player in self.board.get_players() {
let hand = &self.hands.get(&player).unwrap(); let hand = &self.hands.get(&player).unwrap();
try!(f.write_str(&format!("player {}:", player))); try!(f.write_str(&format!("player {}:", player)));
for card in hand.iter() { for (_i, card) in hand.iter() {
try!(f.write_str(&format!(" {}", card))); try!(f.write_str(&format!(" {}", card)));
} }
try!(f.write_str(&"\n")); try!(f.write_str(&"\n"));
@ -571,7 +587,9 @@ impl fmt::Display for GameState {
} }
impl GameState { impl GameState {
pub fn new(opts: &GameOptions, mut deck: Cards) -> GameState { pub fn new(opts: &GameOptions, deck: Cards) -> GameState {
// We enumerate the cards in reverse order since they'll be drawn from the back of the deck.
let mut deck: AnnotatedCards = deck.into_iter().rev().enumerate().rev().collect();
let mut board = BoardState::new(opts, deck.len() as u32); let mut board = BoardState::new(opts, deck.len() as u32);
let hands = let hands =
@ -583,11 +601,15 @@ impl GameState {
}).collect::<Vec<_>>(); }).collect::<Vec<_>>();
(player, hand) (player, hand)
}).collect::<FnvHashMap<_, _>>(); }).collect::<FnvHashMap<_, _>>();
let unannotated_hands = hands.iter().map(|(player, hand)| {
(player.clone(), strip_annotations(hand))
}).collect::<FnvHashMap<_, _>>();
GameState { GameState {
hands: hands, hands,
board: board, unannotated_hands,
deck: deck, board,
deck,
} }
} }
@ -606,7 +628,7 @@ impl GameState {
// get the game state view of a particular player // get the game state view of a particular player
pub fn get_view(&self, player: Player) -> BorrowedGameView { pub fn get_view(&self, player: Player) -> BorrowedGameView {
let mut other_hands = FnvHashMap::default(); let mut other_hands = FnvHashMap::default();
for (&other_player, hand) in &self.hands { for (&other_player, hand) in &self.unannotated_hands {
if player != other_player { if player != other_player {
other_hands.insert(other_player, hand); other_hands.insert(other_player, hand);
} }
@ -619,21 +641,38 @@ impl GameState {
} }
} }
fn update_player_hand(&mut self) {
let player = self.board.player.clone();
self.unannotated_hands.insert(player, strip_annotations(self.hands.get(&player).unwrap()));
}
// takes a card from the player's hand, and replaces it if possible // takes a card from the player's hand, and replaces it if possible
fn take_from_hand(&mut self, index: usize) -> Card { fn take_from_hand(&mut self, index: usize) -> Card {
let ref mut hand = self.hands.get_mut(&self.board.player).unwrap(); // FIXME this code looks like it's awfully contorted in order to please the borrow checker.
hand.remove(index) // Can we have this look nicer?
let result =
{
let ref mut hand = self.hands.get_mut(&self.board.player).unwrap();
hand.remove(index).1
};
self.update_player_hand();
result
} }
fn replenish_hand(&mut self) { fn replenish_hand(&mut self) {
let ref mut hand = self.hands.get_mut(&self.board.player).unwrap(); // FIXME this code looks like it's awfully contorted in order to please the borrow checker.
if (hand.len() as u32) < self.board.hand_size { // Can we have this look nicer?
if let Some(new_card) = self.deck.pop() { {
self.board.deck_size -= 1; let ref mut hand = self.hands.get_mut(&self.board.player).unwrap();
debug!("Drew new card, {}", new_card); if (hand.len() as u32) < self.board.hand_size {
hand.push(new_card); if let Some(new_card) = self.deck.pop() {
self.board.deck_size -= 1;
debug!("Drew new card, {}", new_card.1);
hand.push(new_card);
}
} }
} }
self.update_player_hand();
} }
pub fn process_choice(&mut self, choice: TurnChoice) -> TurnRecord { pub fn process_choice(&mut self, choice: TurnChoice) -> TurnRecord {
@ -651,10 +690,10 @@ impl GameState {
let hand = self.hands.get(&hint.player).unwrap(); let hand = self.hands.get(&hint.player).unwrap();
let results = match hint.hinted { let results = match hint.hinted {
Hinted::Color(color) => { Hinted::Color(color) => {
hand.iter().map(|card| { card.color == color }).collect::<Vec<_>>() hand.iter().map(|(_i, card)| { card.color == color }).collect::<Vec<_>>()
} }
Hinted::Value(value) => { Hinted::Value(value) => {
hand.iter().map(|card| { card.value == value }).collect::<Vec<_>>() hand.iter().map(|(_i, card)| { card.value == value }).collect::<Vec<_>>()
} }
}; };
if !self.board.allow_empty_hints { if !self.board.allow_empty_hints {

56
src/json_output.rs Normal file
View file

@ -0,0 +1,56 @@
use game::*;
use serde_json::*;
fn color_value(color: &Color) -> usize {
COLORS.iter().position(|&card_color| &card_color == color).unwrap()
}
fn card_to_json(card: &Card) -> serde_json::Value {
json!({
"rank": card.value,
"suit": color_value(&card.color),
})
}
pub fn action_clue(hint: &Hint) -> serde_json::Value {
json!({
"type": 0,
"target": hint.player,
"clue": match hint.hinted {
Hinted::Value(value) => { json!({
"type": 0,
"value": value,
}) }
Hinted::Color(color) => { json!({
"type": 1,
"value": color_value(&color),
}) }
}
})
}
pub fn action_play((i, _card): &AnnotatedCard) -> serde_json::Value {
json!({
"type": 1,
"target": i,
})
}
pub fn action_discard((i, _card): &AnnotatedCard) -> serde_json::Value {
json!({
"type": 2,
"target": i,
})
}
pub fn json_format(deck: &Cards, actions: &Vec<serde_json::Value>, players: &Vec<String>) -> serde_json::Value {
json!({
"variant": "No Variant",
"players": players,
"first_player": 0,
"notes": players.iter().map(|_player| {json!([])}).collect::<Vec<_>>(), // TODO add notes
// The deck is reversed since in our implementation we draw from the end of the deck.
"deck": deck.iter().rev().map(card_to_json).collect::<Vec<serde_json::Value>>(),
"actions": actions,
})
}

View file

@ -5,9 +5,11 @@ extern crate rand;
extern crate crossbeam; extern crate crossbeam;
extern crate fnv; extern crate fnv;
extern crate float_ord; extern crate float_ord;
extern crate serde_json;
mod helpers; mod helpers;
mod game; mod game;
mod json_output;
mod simulator; mod simulator;
mod strategy; mod strategy;
mod strategies { mod strategies {
@ -53,6 +55,9 @@ fn main() {
opts.optopt("o", "output", opts.optopt("o", "output",
"Number of games after which to print an update", "Number of games after which to print an update",
"OUTPUT_FREQ"); "OUTPUT_FREQ");
opts.optopt("j", "json-output",
"Pattern for the JSON output file. '%s' will be replaced by the seed.",
"FILE_PATTERN");
opts.optopt("t", "nthreads", opts.optopt("t", "nthreads",
"Number of threads to use for simulation (default 1)", "Number of threads to use for simulation (default 1)",
"NTHREADS"); "NTHREADS");
@ -113,14 +118,24 @@ fn main() {
let seed = matches.opt_str("s").map(|seed_str| { u32::from_str(&seed_str).unwrap() }); let seed = matches.opt_str("s").map(|seed_str| { u32::from_str(&seed_str).unwrap() });
let progress_info = matches.opt_str("o").map(|freq_str| { u32::from_str(&freq_str).unwrap() }); let progress_info = matches.opt_str("o").map(|freq_str| { u32::from_str(&freq_str).unwrap() });
let n_threads = u32::from_str(&matches.opt_str("t").unwrap_or("1".to_string())).unwrap(); let n_threads = u32::from_str(&matches.opt_str("t").unwrap_or("1".to_string())).unwrap();
let json_output_pattern = matches.opt_str("j");
let n_players = u32::from_str(&matches.opt_str("p").unwrap_or("4".to_string())).unwrap(); let n_players = u32::from_str(&matches.opt_str("p").unwrap_or("4".to_string())).unwrap();
let strategy_str : &str = &matches.opt_str("g").unwrap_or("cheat".to_string()); let strategy_str : &str = &matches.opt_str("g").unwrap_or("cheat".to_string());
sim_games(n_players, strategy_str, seed, n_trials, n_threads, progress_info).info(); sim_games(n_players, strategy_str, seed, n_trials, n_threads, progress_info, json_output_pattern).info();
} }
fn sim_games(n_players: u32, strategy_str: &str, seed: Option<u32>, n_trials: u32, n_threads: u32, progress_info: Option<u32>) fn sim_games(
-> simulator::SimResult { n_players: u32,
strategy_str: &str,
seed: Option<u32>,
n_trials: u32,
n_threads: u32,
progress_info: Option<u32>,
json_output_pattern: Option<String>,
) -> simulator::SimResult {
let hand_size = match n_players { let hand_size = match n_players {
2 => 5, 2 => 5,
3 => 5, 3 => 5,
@ -157,7 +172,7 @@ fn sim_games(n_players: u32, strategy_str: &str, seed: Option<u32>, n_trials: u3
panic!("Unexpected strategy argument {}", strategy_str); panic!("Unexpected strategy argument {}", strategy_str);
}, },
}; };
simulator::simulate(&game_opts, strategy_config, seed, n_trials, n_threads, progress_info) simulator::simulate(&game_opts, strategy_config, seed, n_trials, n_threads, progress_info, json_output_pattern)
} }
fn get_results_table() -> String { fn get_results_table() -> String {
@ -193,7 +208,7 @@ fn get_results_table() -> String {
&|n_players| (format_players(n_players), dashes_long.clone())); &|n_players| (format_players(n_players), dashes_long.clone()));
let mut body = strategies.iter().map(|strategy| { let mut body = strategies.iter().map(|strategy| {
make_twolines(&player_nums, (format_name(strategy), space.clone()), &|n_players| { make_twolines(&player_nums, (format_name(strategy), space.clone()), &|n_players| {
let simresult = sim_games(n_players, strategy, Some(seed), n_trials, n_threads, None); let simresult = sim_games(n_players, strategy, Some(seed), n_trials, n_threads, None, None);
( (
format_score(simresult.average_score(), simresult.score_stderr()), format_score(simresult.average_score(), simresult.score_stderr()),
format_percent(simresult.percent_perfect(), simresult.percent_perfect_stderr()) format_percent(simresult.percent_perfect(), simresult.percent_perfect_stderr())

View file

@ -5,6 +5,7 @@ use crossbeam;
use game::*; use game::*;
use strategy::*; use strategy::*;
use json_output::*;
fn new_deck(seed: u32) -> Cards { fn new_deck(seed: u32) -> Cards {
let mut deck: Cards = Cards::new(); let mut deck: Cards = Cards::new();
@ -22,19 +23,24 @@ fn new_deck(seed: u32) -> Cards {
deck deck
} }
pub fn simulate_once( pub fn simulate_once(
opts: &GameOptions, opts: &GameOptions,
game_strategy: Box<GameStrategy>, game_strategy: Box<GameStrategy>,
seed: u32, seed: u32,
) -> GameState { output_json: bool,
) -> (GameState, Option<serde_json::Value>) {
let deck = new_deck(seed); let deck = new_deck(seed);
let mut game = GameState::new(opts, deck); let mut game = GameState::new(opts, deck.clone());
let mut strategies = game.get_players().map(|player| { let mut strategies = game.get_players().map(|player| {
(player, game_strategy.initialize(player, &game.get_view(player))) (player, game_strategy.initialize(player, &game.get_view(player)))
}).collect::<FnvHashMap<Player, Box<PlayerStrategy>>>(); }).collect::<FnvHashMap<Player, Box<PlayerStrategy>>>();
let mut actions = Vec::new();
while !game.is_over() { while !game.is_over() {
let player = game.board.player; let player = game.board.player;
@ -49,6 +55,22 @@ pub fn simulate_once(
let mut strategy = strategies.get_mut(&player).unwrap(); let mut strategy = strategies.get_mut(&player).unwrap();
strategy.decide(&game.get_view(player)) strategy.decide(&game.get_view(player))
}; };
if output_json {
actions.push(match choice {
TurnChoice::Hint(ref hint) => {
action_clue(hint)
}
TurnChoice::Play(index) => {
let card = &game.hands[&player][index];
action_play(card)
}
TurnChoice::Discard(index) => {
let card = &game.hands[&player][index];
action_discard(card)
}
});
}
let turn = game.process_choice(choice); let turn = game.process_choice(choice);
@ -56,13 +78,20 @@ pub fn simulate_once(
let mut strategy = strategies.get_mut(&player).unwrap(); let mut strategy = strategies.get_mut(&player).unwrap();
strategy.update(&turn, &game.get_view(player)); strategy.update(&turn, &game.get_view(player));
} }
} }
debug!(""); debug!("");
debug!("======================================================="); debug!("=======================================================");
debug!("Final state:\n{}", game); debug!("Final state:\n{}", game);
debug!("SCORE: {:?}", game.score()); debug!("SCORE: {:?}", game.score());
game let json_output = if output_json {
let player_names = game.get_players().map(|player| {
strategies[&player].name()
}).collect();
Some(json_format(&deck, &actions, &player_names))
} else {
None
};
(game, json_output)
} }
#[derive(Debug)] #[derive(Debug)]
@ -134,12 +163,14 @@ pub fn simulate<T: ?Sized>(
n_trials: u32, n_trials: u32,
n_threads: u32, n_threads: u32,
progress_info: Option<u32>, progress_info: Option<u32>,
json_output_pattern: Option<String>,
) -> SimResult ) -> SimResult
where T: GameStrategyConfig + Sync { where T: GameStrategyConfig + Sync {
let first_seed = first_seed_opt.unwrap_or_else(|| rand::thread_rng().next_u32()); let first_seed = first_seed_opt.unwrap_or_else(|| rand::thread_rng().next_u32());
let strat_config_ref = &strat_config; let strat_config_ref = &strat_config;
let json_output_pattern_ref = &json_output_pattern;
crossbeam::scope(|scope| { crossbeam::scope(|scope| {
let mut join_handles = Vec::new(); let mut join_handles = Vec::new();
for i in 0..n_threads { for i in 0..n_threads {
@ -164,11 +195,23 @@ pub fn simulate<T: ?Sized>(
); );
} }
} }
let game = simulate_once(&opts, strat_config_ref.initialize(&opts), seed); let (game, json_output) = simulate_once(&opts,
strat_config_ref.initialize(&opts),
seed,
json_output_pattern_ref.is_some());
let score = game.score(); let score = game.score();
lives_histogram.insert(game.board.lives_remaining); lives_histogram.insert(game.board.lives_remaining);
score_histogram.insert(score); score_histogram.insert(score);
if score != PERFECT_SCORE { non_perfect_seeds.push(seed); } if score != PERFECT_SCORE { non_perfect_seeds.push(seed); }
match json_output_pattern_ref {
Some(file_pattern) => {
let file_pattern = file_pattern.clone().replace("%s", &seed.to_string());
let path = std::path::Path::new(&file_pattern);
let file = std::fs::File::create(path).unwrap();
serde_json::to_writer(file, &json_output.unwrap()).unwrap();
}
None => { }
}
} }
if progress_info.is_some() { if progress_info.is_some() {
info!("Thread {} done", i); info!("Thread {} done", i);

View file

@ -136,6 +136,9 @@ impl CheatingPlayerStrategy {
} }
} }
impl PlayerStrategy for CheatingPlayerStrategy { impl PlayerStrategy for CheatingPlayerStrategy {
fn name(&self) -> String {
String::from("cheat")
}
fn decide(&mut self, view: &BorrowedGameView) -> TurnChoice { fn decide(&mut self, view: &BorrowedGameView) -> TurnChoice {
self.inform_last_player_cards(view); self.inform_last_player_cards(view);

View file

@ -39,6 +39,9 @@ pub struct RandomStrategyPlayer {
} }
impl PlayerStrategy for RandomStrategyPlayer { impl PlayerStrategy for RandomStrategyPlayer {
fn name(&self) -> String {
format!("random(hint={}, play={})", self.hint_probability, self.play_probability)
}
fn decide(&mut self, view: &BorrowedGameView) -> TurnChoice { fn decide(&mut self, view: &BorrowedGameView) -> TurnChoice {
let p = rand::random::<f64>(); let p = rand::random::<f64>();
if p < self.hint_probability { if p < self.hint_probability {

View file

@ -913,6 +913,10 @@ impl InformationPlayerStrategy {
} }
impl PlayerStrategy for InformationPlayerStrategy { impl PlayerStrategy for InformationPlayerStrategy {
fn name(&self) -> String {
String::from("info")
}
fn decide(&mut self, _: &BorrowedGameView) -> TurnChoice { fn decide(&mut self, _: &BorrowedGameView) -> TurnChoice {
let mut public_info = self.public_info.clone(); let mut public_info = self.public_info.clone();
let turn_choice = self.decide_wrapped(&mut public_info); let turn_choice = self.decide_wrapped(&mut public_info);

View file

@ -4,6 +4,11 @@ use game::*;
// Represents the strategy of a given player // Represents the strategy of a given player
pub trait PlayerStrategy { pub trait PlayerStrategy {
// A function returning the name of a strategy.
// This is a method of PlayerStrategy rather than GameStrategyConfig
// so that the name may incorporate useful information that's specific
// to this player instance.
fn name(&self) -> String;
// A function to decide what to do on the player's turn. // A function to decide what to do on the player's turn.
// Given a BorrowedGameView, outputs their choice. // Given a BorrowedGameView, outputs their choice.
fn decide(&mut self, &BorrowedGameView) -> TurnChoice; fn decide(&mut self, &BorrowedGameView) -> TurnChoice;