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:
parent
7f384cc15d
commit
180d8c5f4e
10 changed files with 228 additions and 29 deletions
30
Cargo.lock
generated
30
Cargo.lock
generated
|
@ -18,6 +18,11 @@ name = "getopts"
|
|||
version = "0.2.14"
|
||||
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]]
|
||||
name = "libc"
|
||||
version = "0.2.7"
|
||||
|
@ -49,6 +54,27 @@ dependencies = [
|
|||
"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)",
|
||||
"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]
|
||||
|
@ -56,6 +82,10 @@ dependencies = [
|
|||
"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 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 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 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"
|
||||
|
|
|
@ -10,3 +10,4 @@ getopts = "*"
|
|||
fnv = "*"
|
||||
float-ord = "*"
|
||||
crossbeam = "0.2.5"
|
||||
serde_json = "*"
|
||||
|
|
77
src/game.rs
77
src/game.rs
|
@ -389,7 +389,7 @@ impl BoardState {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -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!)
|
||||
#[derive(Debug)]
|
||||
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 deck: Cards,
|
||||
pub deck: AnnotatedCards,
|
||||
}
|
||||
impl fmt::Display for GameState {
|
||||
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() {
|
||||
let hand = &self.hands.get(&player).unwrap();
|
||||
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(&"\n"));
|
||||
|
@ -571,7 +587,9 @@ impl fmt::Display for 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 hands =
|
||||
|
@ -583,11 +601,15 @@ impl GameState {
|
|||
}).collect::<Vec<_>>();
|
||||
(player, hand)
|
||||
}).collect::<FnvHashMap<_, _>>();
|
||||
let unannotated_hands = hands.iter().map(|(player, hand)| {
|
||||
(player.clone(), strip_annotations(hand))
|
||||
}).collect::<FnvHashMap<_, _>>();
|
||||
|
||||
GameState {
|
||||
hands: hands,
|
||||
board: board,
|
||||
deck: deck,
|
||||
hands,
|
||||
unannotated_hands,
|
||||
board,
|
||||
deck,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -606,7 +628,7 @@ impl GameState {
|
|||
// get the game state view of a particular player
|
||||
pub fn get_view(&self, player: Player) -> BorrowedGameView {
|
||||
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 {
|
||||
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
|
||||
fn take_from_hand(&mut self, index: usize) -> Card {
|
||||
let ref mut hand = self.hands.get_mut(&self.board.player).unwrap();
|
||||
hand.remove(index)
|
||||
// FIXME this code looks like it's awfully contorted in order to please the borrow checker.
|
||||
// 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) {
|
||||
let ref mut hand = self.hands.get_mut(&self.board.player).unwrap();
|
||||
if (hand.len() as u32) < self.board.hand_size {
|
||||
if let Some(new_card) = self.deck.pop() {
|
||||
self.board.deck_size -= 1;
|
||||
debug!("Drew new card, {}", new_card);
|
||||
hand.push(new_card);
|
||||
// FIXME this code looks like it's awfully contorted in order to please the borrow checker.
|
||||
// Can we have this look nicer?
|
||||
{
|
||||
let ref mut hand = self.hands.get_mut(&self.board.player).unwrap();
|
||||
if (hand.len() as u32) < self.board.hand_size {
|
||||
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 {
|
||||
|
@ -651,10 +690,10 @@ impl GameState {
|
|||
let hand = self.hands.get(&hint.player).unwrap();
|
||||
let results = match hint.hinted {
|
||||
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) => {
|
||||
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 {
|
||||
|
|
56
src/json_output.rs
Normal file
56
src/json_output.rs
Normal 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,
|
||||
})
|
||||
}
|
25
src/main.rs
25
src/main.rs
|
@ -5,9 +5,11 @@ extern crate rand;
|
|||
extern crate crossbeam;
|
||||
extern crate fnv;
|
||||
extern crate float_ord;
|
||||
extern crate serde_json;
|
||||
|
||||
mod helpers;
|
||||
mod game;
|
||||
mod json_output;
|
||||
mod simulator;
|
||||
mod strategy;
|
||||
mod strategies {
|
||||
|
@ -53,6 +55,9 @@ fn main() {
|
|||
opts.optopt("o", "output",
|
||||
"Number of games after which to print an update",
|
||||
"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",
|
||||
"Number of threads to use for simulation (default 1)",
|
||||
"NTHREADS");
|
||||
|
@ -113,14 +118,24 @@ fn main() {
|
|||
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 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 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>)
|
||||
-> simulator::SimResult {
|
||||
fn sim_games(
|
||||
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 {
|
||||
2 => 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);
|
||||
},
|
||||
};
|
||||
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 {
|
||||
|
@ -193,7 +208,7 @@ fn get_results_table() -> String {
|
|||
&|n_players| (format_players(n_players), dashes_long.clone()));
|
||||
let mut body = strategies.iter().map(|strategy| {
|
||||
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_percent(simresult.percent_perfect(), simresult.percent_perfect_stderr())
|
||||
|
|
|
@ -5,6 +5,7 @@ use crossbeam;
|
|||
|
||||
use game::*;
|
||||
use strategy::*;
|
||||
use json_output::*;
|
||||
|
||||
fn new_deck(seed: u32) -> Cards {
|
||||
let mut deck: Cards = Cards::new();
|
||||
|
@ -22,19 +23,24 @@ fn new_deck(seed: u32) -> Cards {
|
|||
deck
|
||||
}
|
||||
|
||||
|
||||
pub fn simulate_once(
|
||||
opts: &GameOptions,
|
||||
game_strategy: Box<GameStrategy>,
|
||||
seed: u32,
|
||||
) -> GameState {
|
||||
output_json: bool,
|
||||
) -> (GameState, Option<serde_json::Value>) {
|
||||
|
||||
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| {
|
||||
(player, game_strategy.initialize(player, &game.get_view(player)))
|
||||
}).collect::<FnvHashMap<Player, Box<PlayerStrategy>>>();
|
||||
|
||||
let mut actions = Vec::new();
|
||||
|
||||
while !game.is_over() {
|
||||
let player = game.board.player;
|
||||
|
||||
|
@ -49,6 +55,22 @@ pub fn simulate_once(
|
|||
let mut strategy = strategies.get_mut(&player).unwrap();
|
||||
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);
|
||||
|
||||
|
@ -56,13 +78,20 @@ pub fn simulate_once(
|
|||
let mut strategy = strategies.get_mut(&player).unwrap();
|
||||
strategy.update(&turn, &game.get_view(player));
|
||||
}
|
||||
|
||||
}
|
||||
debug!("");
|
||||
debug!("=======================================================");
|
||||
debug!("Final state:\n{}", game);
|
||||
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)]
|
||||
|
@ -134,12 +163,14 @@ pub fn simulate<T: ?Sized>(
|
|||
n_trials: u32,
|
||||
n_threads: u32,
|
||||
progress_info: Option<u32>,
|
||||
json_output_pattern: Option<String>,
|
||||
) -> SimResult
|
||||
where T: GameStrategyConfig + Sync {
|
||||
|
||||
let first_seed = first_seed_opt.unwrap_or_else(|| rand::thread_rng().next_u32());
|
||||
|
||||
let strat_config_ref = &strat_config;
|
||||
let json_output_pattern_ref = &json_output_pattern;
|
||||
crossbeam::scope(|scope| {
|
||||
let mut join_handles = Vec::new();
|
||||
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();
|
||||
lives_histogram.insert(game.board.lives_remaining);
|
||||
score_histogram.insert(score);
|
||||
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() {
|
||||
info!("Thread {} done", i);
|
||||
|
|
|
@ -136,6 +136,9 @@ impl CheatingPlayerStrategy {
|
|||
}
|
||||
}
|
||||
impl PlayerStrategy for CheatingPlayerStrategy {
|
||||
fn name(&self) -> String {
|
||||
String::from("cheat")
|
||||
}
|
||||
fn decide(&mut self, view: &BorrowedGameView) -> TurnChoice {
|
||||
self.inform_last_player_cards(view);
|
||||
|
||||
|
|
|
@ -39,6 +39,9 @@ pub struct 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 {
|
||||
let p = rand::random::<f64>();
|
||||
if p < self.hint_probability {
|
||||
|
|
|
@ -913,6 +913,10 @@ impl InformationPlayerStrategy {
|
|||
}
|
||||
|
||||
impl PlayerStrategy for InformationPlayerStrategy {
|
||||
fn name(&self) -> String {
|
||||
String::from("info")
|
||||
}
|
||||
|
||||
fn decide(&mut self, _: &BorrowedGameView) -> TurnChoice {
|
||||
let mut public_info = self.public_info.clone();
|
||||
let turn_choice = self.decide_wrapped(&mut public_info);
|
||||
|
|
|
@ -4,6 +4,11 @@ use game::*;
|
|||
|
||||
// Represents the strategy of a given player
|
||||
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.
|
||||
// Given a BorrowedGameView, outputs their choice.
|
||||
fn decide(&mut self, &BorrowedGameView) -> TurnChoice;
|
||||
|
|
Loading…
Reference in a new issue