Merge pull request #16 from timotree3/json_output_2021

Updated JSON output for hanab.live
This commit is contained in:
Jeff Wu 2023-01-19 20:59:54 -08:00 committed by GitHub
commit 2d1a6163a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 246 additions and 17 deletions

3
.gitignore vendored
View File

@ -1,2 +1,5 @@
target
*.sw*
# Developers may wish to generate JSON replays of bot games and store them in /replays/
/replays/

30
Cargo.lock generated
View File

@ -41,6 +41,12 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "itoa"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
[[package]]
name = "libc"
version = "0.2.125"
@ -122,6 +128,30 @@ dependencies = [
"getopts",
"log 0.3.9",
"rand 0.3.23",
"serde_json",
]
[[package]]
name = "ryu"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
[[package]]
name = "serde"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
[[package]]
name = "serde_json"
version = "1.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]

View File

@ -11,3 +11,4 @@ getopts = "0.2.14"
fnv = "1.0.0"
float-ord = "0.2.0"
crossbeam = "0.2.5"
serde_json = "*"

View File

@ -368,7 +368,9 @@ 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 {
@ -522,12 +524,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 {
@ -538,7 +556,7 @@ impl fmt::Display for GameState {
for player in self.board.get_players() {
let hand = &self.hands.get(&player).unwrap();
write!(f, "player {player}:")?;
for card in hand.iter() {
for (_i, card) in hand.iter() {
write!(f, " {card}")?;
}
f.write_str("\n")?;
@ -552,7 +570,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 = (0..opts.num_players)
@ -567,8 +587,17 @@ impl GameState {
(player, hand)
})
.collect::<FnvHashMap<_, _>>();
let unannotated_hands = hands
.iter()
.map(|(player, hand)| (*player, strip_annotations(hand)))
.collect::<FnvHashMap<_, _>>();
GameState { hands, board, deck }
GameState {
hands,
unannotated_hands,
board,
deck,
}
}
pub fn get_players(&self) -> Range<Player> {
@ -586,7 +615,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);
}
@ -599,10 +628,18 @@ impl GameState {
}
}
fn update_player_hand(&mut self) {
let player = self.board.player;
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 hand = &mut self.hands.get_mut(&self.board.player).unwrap();
hand.remove(index)
let card = hand.remove(index).1;
self.update_player_hand();
card
}
fn replenish_hand(&mut self) {
@ -610,10 +647,11 @@ impl GameState {
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);
debug!("Drew new card, {}", new_card.1);
hand.push(new_card);
}
}
self.update_player_hand();
}
pub fn process_choice(&mut self, choice: TurnChoice) -> TurnRecord {
@ -637,11 +675,11 @@ impl GameState {
let results = match hint.hinted {
Hinted::Color(color) => hand
.iter()
.map(|card| card.color == color)
.map(|(_i, card)| card.color == color)
.collect::<Vec<_>>(),
Hinted::Value(value) => hand
.iter()
.map(|card| card.value == value)
.map(|(_i, card)| card.value == value)
.collect::<Vec<_>>(),
};
if !self.board.allow_empty_hints {

67
src/json_output.rs Normal file
View File

@ -0,0 +1,67 @@
use crate::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,
"suitIndex": color_value(&card.color),
})
}
pub fn action_clue(hint: &Hint) -> serde_json::Value {
match hint.hinted {
Hinted::Color(color) => {
json!({
"type": 2,
"target": hint.player,
"value": color_value(&color),
})
}
Hinted::Value(value) => {
json!({
"type": 3,
"target": hint.player,
"value": value,
})
}
}
}
pub fn action_play((i, _card): &AnnotatedCard) -> serde_json::Value {
json!({
"type": 0,
"target": i,
})
}
pub fn action_discard((i, _card): &AnnotatedCard) -> serde_json::Value {
json!({
"type": 1,
"target": i,
})
}
pub fn json_format(
deck: &Cards,
actions: &Vec<serde_json::Value>,
players: &Vec<String>,
) -> serde_json::Value {
json!({
"options": {
"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 crossbeam;
extern crate float_ord;
extern crate fnv;
extern crate rand;
extern crate serde_json;
mod game;
mod helpers;
mod json_output;
mod simulator;
mod strategy;
mod strategies {
@ -60,6 +62,12 @@ fn main() {
"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",
@ -85,6 +93,11 @@ fn main() {
"write-results-table",
"Update the results table in README.md",
);
opts.optflag(
"",
"losses-only",
"When saving JSON outputs, save lost games only",
);
let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
Err(f) => {
@ -136,6 +149,8 @@ fn main() {
let n_players = u32::from_str(matches.opt_str("p").as_deref().unwrap_or("4")).unwrap();
let g_opt = matches.opt_str("g");
let strategy_str: &str = g_opt.as_deref().unwrap_or("cheat");
let json_output_pattern = matches.opt_str("j");
let json_losses_only = matches.opt_present("losses-only");
sim_games(
n_players,
@ -144,6 +159,8 @@ fn main() {
n_trials,
n_threads,
progress_info,
json_output_pattern,
json_losses_only,
)
.info();
}
@ -155,6 +172,8 @@ fn sim_games(
n_trials: u32,
n_threads: u32,
progress_info: Option<u32>,
json_output_pattern: Option<String>,
json_losses_only: bool,
) -> simulator::SimResult {
let hand_size = match n_players {
2 => 5,
@ -195,6 +214,8 @@ fn sim_games(
n_trials,
n_threads,
progress_info,
json_output_pattern,
json_losses_only,
)
}
@ -250,8 +271,16 @@ fn get_results_table() -> String {
&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,
false,
);
(
format_score(simresult.average_score(), simresult.score_stderr()),
format_percent(

View File

@ -3,6 +3,7 @@ use rand::{self, Rng, SeedableRng};
use std::fmt;
use crate::game::*;
use crate::json_output::*;
use crate::strategy::*;
fn new_deck(seed: u32) -> Cards {
@ -25,10 +26,11 @@ pub fn simulate_once(
opts: &GameOptions,
game_strategy: Box<dyn 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()
@ -40,6 +42,8 @@ pub fn simulate_once(
})
.collect::<FnvHashMap<Player, Box<dyn PlayerStrategy>>>();
let mut actions = Vec::new();
while !game.is_over() {
let player = game.board.player;
@ -53,6 +57,19 @@ pub fn simulate_once(
let 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);
@ -65,7 +82,16 @@ pub fn simulate_once(
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)]
@ -135,6 +161,8 @@ pub fn simulate<T: ?Sized>(
n_trials: u32,
n_threads: u32,
progress_info: Option<u32>,
json_output_pattern: Option<String>,
json_losses_only: bool,
) -> SimResult
where
T: GameStrategyConfig + Sync,
@ -142,6 +170,7 @@ where
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 {
@ -169,13 +198,27 @@ where
);
}
}
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);
}
if let Some(file_pattern) = json_output_pattern_ref {
if !(score == PERFECT_SCORE && json_losses_only) {
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();
}
}
}
if progress_info.is_some() {
info!("Thread {} done", i);

View File

@ -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);

View File

@ -39,6 +39,12 @@ 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.play_probability {

View File

@ -991,6 +991,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);

View File

@ -4,6 +4,11 @@ use crate::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, view: &BorrowedGameView) -> TurnChoice;