use cfr::{ChanceNode, Game, Node, Player, PlayerNode, Solution, TerminalNode};
use clap::Parser;
use serde::{Deserialize, Serialize};
use std::collections::{btree_map, BTreeMap, HashMap};
use std::io;
use std::iter;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[clap(short = 'r', long, value_parser, default_value_t = 0.0)]
max_regret: f64,
#[clap(short = 'i', long, value_parser, default_value_t = 1000)]
max_iters: u64,
#[clap(short = 'p', long, value_parser, default_value_t = 0.0)]
prune_threshold: f64,
}
type StateNode = Node<Terminal, Chance, Actor>;
#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
enum State {
Terminal(Terminal),
Chance(Chance),
Player(Actor),
}
impl Into<StateNode> for State {
fn into(self) -> StateNode {
match self {
State::Terminal(term) => StateNode::Terminal(term),
State::Chance(chance) => StateNode::Chance(chance),
State::Player(player) => StateNode::Player(player),
}
}
}
#[derive(Deserialize)]
struct Terminal(f64);
impl TerminalNode for Terminal {
fn get_one_payoff(&self) -> f64 {
let Terminal(payoff) = self;
*payoff
}
}
#[derive(Deserialize)]
struct Chance(BTreeMap<String, Outcome>);
fn to_chance_node(outcome: Outcome) -> (f64, StateNode) {
let Outcome { prob, state } = outcome;
(prob, state.into())
}
impl IntoIterator for Chance {
type Item = (f64, StateNode);
type IntoIter = iter::Map<btree_map::IntoValues<String, Outcome>, fn(Outcome) -> Self::Item>;
fn into_iter(self) -> Self::IntoIter {
let Chance(tree) = self;
tree.into_values().map(to_chance_node)
}
}
impl ChanceNode<Terminal, Actor> for Chance {}
#[derive(Deserialize)]
struct Outcome {
prob: f64,
state: State,
}
#[derive(Deserialize)]
struct Actor {
player_one: bool,
infoset: String,
actions: BTreeMap<String, State>,
}
fn to_actor_node(entry: (String, State)) -> (String, StateNode) {
let (key, state) = entry;
(key, state.into())
}
impl IntoIterator for Actor {
type Item = (String, StateNode);
type IntoIter =
iter::Map<btree_map::IntoIter<String, State>, fn((String, State)) -> Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.actions.into_iter().map(to_actor_node)
}
}
impl PlayerNode<String, String, Terminal, Chance> for Actor {
fn get_player(&self) -> Player {
if self.player_one {
Player::One
} else {
Player::Two
}
}
fn get_infoset(&self) -> String {
self.infoset.clone()
}
}
#[derive(Serialize)]
struct Strategy(HashMap<String, HashMap<String, f64>>);
impl<'a, I, A> From<I> for Strategy
where
I: IntoIterator<Item = (&'a String, A)>,
A: IntoIterator<Item = (&'a String, &'a f64)>,
{
fn from(named_strategies: I) -> Self {
let mut map = HashMap::new();
for (info, actions) in named_strategies {
assert!(
map.insert(
info.clone(),
actions
.into_iter()
.filter(|(_, p)| p > &&0.0)
.map(|(a, p)| (a.clone(), p.clone()))
.collect()
)
.is_none(),
"internal error: found duplicate infosets"
);
}
Strategy(map)
}
}
#[derive(Serialize)]
struct Output {
expected_utility: f64,
player_one_regret: f64,
player_two_regret: f64,
regret: f64,
player_one_strategy: Strategy,
player_two_strategy: Strategy,
}
fn main() {
let args = Args::parse();
let definition: State = serde_json::from_reader(io::stdin())
.expect("couln't parse game definition, try looking at the readme: https://github.com/erikbrinkman/cfr#json-error");
let game = Game::from_node(definition.into()).expect("couldn't extract a compact game representation due to problems with the structure, try looking at the readme: https://github.com/erikbrinkman/cfr#json-error");
let Solution { strategy, .. } = game.solve(args.max_iters, args.max_regret);
let regret = game
.regret(&strategy)
.expect("internal error: got invalid strategy, please report at: https://github.com/erikbrinkman/cfr/issues");
let mut pruned_strat = Vec::with_capacity(strategy.len());
for strat in strategy.iter() {
let total: f64 = strat
.iter()
.map(|v| if *v < args.prune_threshold { 0.0 } else { *v })
.sum();
let pruned: Vec<_> = strat
.iter()
.map(|v| {
if *v < args.prune_threshold {
0.0
} else {
*v / total
}
})
.collect();
pruned_strat.push(pruned);
}
let pruned_regret = game
.regret(&pruned_strat)
.expect("internal error: pruned strategy incorrectly, please report at: https://github.com/erikbrinkman/cfr/issues");
let (info, one, two): (_, Strategy, Strategy) = if regret.regret() < pruned_regret.regret() {
let (one, two) = game.name_strategy(&strategy);
(regret, one.into(), two.into())
} else {
let (one, two) = game.name_strategy(&pruned_strat);
(pruned_regret, one.into(), two.into())
};
let out = Output {
expected_utility: info.utility,
player_one_regret: info.player_one_regret,
player_two_regret: info.player_two_regret,
regret: info.regret(),
player_one_strategy: one.into(),
player_two_strategy: two.into(),
};
serde_json::to_writer(io::stdout(), &out).expect("error writing strategies to stdout");
}