use anyhow::{Context as _, Result, bail};
use gin_rummy::{Card, Hand, Phase, Player, Rank, RoundResult, Rules, Suit, best_melds, deadwood};
use gin_rummy_engine::{
Assessment, DrawAction, EaaiSimpleBot, EngineError, HeuristicBot, HeuristicConfig, Layoff,
MonteCarloBot, Strategy, Table, TurnAction, UpcardAction, View,
};
use rand::rngs::StdRng;
use rand::{RngExt as _, SeedableRng};
use std::io::Write as _;
const HUMAN: Player = Player::One;
fn parse_args() -> Result<(String, Rules, Option<u64>)> {
let mut bot = "mc".to_string();
let mut rules = Rules::default();
let mut seed = None;
let mut args = std::env::args().skip(1);
while let Some(flag) = args.next() {
let mut value = || args.next().with_context(|| format!("{flag} needs a value"));
match flag.as_str() {
"--bot" => bot = value()?,
"--seed" => seed = Some(value()?.parse()?),
"--rules" => {
rules = match value()?.as_str() {
"modern" => Rules::new(),
"classic" => Rules::classic(),
"palace" => Rules::palace(),
other => bail!("unknown rules preset {other:?}"),
}
}
other => bail!("unknown flag {other:?} (--bot/--seed/--rules)"),
}
}
Ok((bot, rules, seed))
}
fn make_bot(spec: &str, rng: &mut StdRng) -> Result<Box<dyn Strategy>> {
let (kind, samples) = match spec.split_once(':') {
Some((kind, samples)) => (kind, Some(samples.parse::<u32>()?)),
None => (spec, None),
};
match kind {
"newbie" => {
let mut config = HeuristicConfig::default();
config.knock_threshold = 10;
config.safety_weight = 0;
config.score_awareness = 0;
Ok(Box::new(HeuristicBot::with_config(config)))
}
"greedy" => Ok(Box::new(HeuristicBot::new())),
"eaai" => Ok(Box::new(EaaiSimpleBot::new(StdRng::seed_from_u64(
rng.random(),
)))),
"mc" => Ok(Box::new(
MonteCarloBot::new(StdRng::seed_from_u64(rng.random())).samples(samples.unwrap_or(64)),
)),
other => bail!("unknown bot {other:?} (newbie | greedy | eaai | mc[:samples])"),
}
}
fn read_command(prompt: &str) -> Option<String> {
print!("{prompt} ");
std::io::stdout().flush().ok()?;
let mut line = String::new();
match std::io::stdin().read_line(&mut line) {
Ok(0) | Err(_) => {
println!();
None
}
Ok(_) => Some(line.trim().to_lowercase()),
}
}
fn resolve_card(text: &str, hand: Hand) -> Option<Card> {
if let Ok(card) = text.parse::<Card>() {
return Some(card);
}
let matches: Vec<Card> = if let Ok(rank) = text.parse::<Rank>() {
hand.iter().filter(|c| c.rank == rank).collect()
} else if let Ok(suit) = text.parse::<Suit>() {
hand.iter().filter(|c| c.suit == suit).collect()
} else {
Vec::new()
};
match matches.as_slice() {
[card] => Some(*card),
[] => {
println!("Cannot read {text:?} as a card; try forms like S10 or ♠T.");
None
}
many => {
let names: Vec<String> = many.iter().map(Card::to_string).collect();
println!("{text:?} matches several cards: {}.", names.join(" "));
None
}
}
}
fn best_shed(hand: Hand, taken: Option<Card>) -> Card {
hand.iter()
.filter(|&c| Some(c) != taken)
.min_by_key(|&c| (deadwood(hand - c.into()), u8::MAX - c.rank.deadwood()))
.expect("a full hand always has a legal discard")
}
fn emphasize(text: &str, card: Card) -> String {
let name = card.to_string();
text.replace(&name, &format!("\x1b[7m{name}\x1b[0m"))
}
fn list(cards: gin_rummy::Hand) -> String {
cards
.iter()
.map(|card| card.to_string())
.collect::<Vec<_>>()
.join(" ")
}
fn sorted(cards: gin_rummy::Hand, by_suit: bool) -> String {
let mut cards: Vec<Card> = cards.iter().collect();
if !by_suit {
cards.sort_by_key(|card| (card.rank, card.suit));
}
cards
.iter()
.map(|card| card.to_string())
.collect::<Vec<_>>()
.join(" ")
}
fn print_hand(view: &View<'_>, by_suit: bool, drawn: Option<Card>) {
let melds = view.best_melds();
let arranged = melds
.iter()
.map(|m| m.to_string())
.collect::<Vec<_>>()
.join(" ");
let loose = melds.deadwood_cards();
let sep = if !arranged.is_empty() && !loose.is_empty() {
" | "
} else {
""
};
let line = format!(
"Your hand: {arranged}{sep}{} ({} deadwood)",
sorted(loose, by_suit),
melds.deadwood(),
);
match drawn {
Some(card) => println!("{}", emphasize(&line, card)),
None => println!("{line}"),
}
}
fn show_position(view: &View<'_>, by_suit: bool, drawn: Option<Card>) {
println!();
print_hand(view, by_suit, drawn);
match view.upcard() {
Some(top) => println!(
"Pile top: {top} (pile of {}), stock: {} cards",
view.discard_pile().len(),
view.stock_len(),
),
None => println!("Pile empty, stock: {} cards", view.stock_len()),
}
if !view.opponent_known().is_empty() {
println!("Bot is holding: {}", list(view.opponent_known()));
}
}
fn print_hints(rows: &[Assessment]) {
if rows.is_empty() {
println!("Nothing to weigh here — the move is forced.");
return;
}
println!("Solver — equity is your chance to win the game, EV your expected round points:");
if !rows[0].recommended {
println!("(Equities within the sampling noise are ties; the bot holds its default move.)");
}
for row in rows {
let mark = if row.recommended { "▸" } else { " " };
let line = format!(
" {mark} {:<15} {:>5.1}% EV {:+5.1}",
row.action,
row.equity * 100.0,
row.ev,
);
if row.recommended {
println!("\x1b[7m{line}\x1b[0m");
} else {
println!("{line}");
}
}
}
struct HumanCli {
by_suit: bool,
predraw: Option<Hand>,
hinter: MonteCarloBot<StdRng>,
}
impl HumanCli {
fn toggle_sort(&mut self, view: &View<'_>) {
self.by_suit = !self.by_suit;
print_hand(view, self.by_suit, self.drawn(view));
}
fn drawn(&self, view: &View<'_>) -> Option<Card> {
self.predraw
.and_then(|before| (view.hand() - before).iter().next())
}
}
impl Strategy for HumanCli {
fn offer_upcard(&mut self, view: &View<'_>) -> UpcardAction {
self.predraw = Some(view.hand());
show_position(view, self.by_suit, self.drawn(view));
loop {
match read_command(
"Take the upcard or pass? [take/pass/\x1b[7mh\x1b[0mint/\x1b[7mv\x1b[0miew]",
)
.unwrap_or_else(|| "quit".into())
.as_str()
{
"take" | "t" => return UpcardAction::Take,
"pass" | "p" => return UpcardAction::Pass,
"hint" | "h" => print_hints(&self.hinter.assess(view)),
"sort" | "view" | "v" => self.toggle_sort(view),
"quit" => std::process::exit(0),
_ => println!("Commands: take, pass, hint, sort/view, quit."),
}
}
}
fn choose_draw(&mut self, view: &View<'_>) -> DrawAction {
self.predraw = Some(view.hand());
show_position(view, self.by_suit, self.drawn(view));
loop {
match read_command(
"Draw from the stock or take the pile top? [draw/take/\x1b[7mh\x1b[0mint/\x1b[7mv\x1b[0miew]",
)
.unwrap_or_else(|| "quit".into())
.as_str()
{
"draw" | "d" => return DrawAction::Stock,
"take" | "t" => return DrawAction::TakeDiscard,
"hint" | "h" => print_hints(&self.hinter.assess(view)),
"sort" | "view" | "v" => self.toggle_sort(view),
"quit" => std::process::exit(0),
_ => println!("Commands: draw, take, hint, sort/view, quit."),
}
}
}
fn play_turn(&mut self, view: &View<'_>) -> TurnAction {
show_position(view, self.by_suit, self.drawn(view));
if view.deadwood() == 0 && view.rules().big_gin_bonus.is_some() {
println!("You have BIG GIN!");
return TurnAction::BigGin(view.best_melds());
}
if let Some(card) = view.taken_discard() {
println!("(The just-taken {card} may not be shed this turn.)");
}
loop {
match read_command(
"Your move [<card> to discard / k\x1b[7mn\x1b[0mock / hint / \x1b[7mv\x1b[0miew]:",
)
.unwrap_or_else(|| "quit".into())
.as_str()
{
"knock" | "n" => {
let discard = best_shed(view.hand(), view.taken_discard());
return TurnAction::Knock {
discard,
melds: best_melds(view.hand() - discard.into()),
};
}
"hint" => print_hints(&self.hinter.assess(view)),
"sort" | "view" | "v" => self.toggle_sort(view),
"quit" => std::process::exit(0),
text => {
if let Some(card) = resolve_card(text, view.hand()) {
return TurnAction::Discard(card);
}
}
}
}
}
fn choose_layoff(&mut self, view: &View<'_>) -> Option<Layoff> {
let layoff = HeuristicBot::new().choose_layoff(view);
if let Some(l) = &layoff {
println!("You lay off {}.", l.card);
}
layoff
}
fn name(&self) -> &str {
"you"
}
}
fn narrate_bot(before_phase: Phase, before_top: Option<Card>, before_pile: usize, table: &Table) {
let round = table.round();
match before_phase {
Phase::Upcard => {
if round.phase() == Phase::Discard {
println!(
"Bot takes the {}.",
before_top.expect("a take implies a pile top"),
);
} else {
println!("Bot passes.");
}
}
Phase::Draw => {
if round.discard_pile().len() < before_pile {
println!(
"Bot takes the {}.",
before_top.expect("a take implies a pile top"),
);
} else {
println!("Bot draws from the stock.");
}
}
Phase::Discard => {
let top = round.discard_pile().last().copied();
match round.result() {
Some(RoundResult::BigGin { .. }) => println!("Bot declares BIG GIN!"),
Some(RoundResult::Gin { .. }) => {
println!(
"Bot discards {} and goes gin!",
top.expect("gin sheds a discard"),
);
}
_ if round.knocker() == Some(Player::Two) => {
println!(
"Bot discards {} and knocks.",
top.expect("a knock sheds a discard"),
);
let spread = table
.view(HUMAN)
.spread()
.map(|meld| meld.to_string())
.collect::<Vec<_>>()
.join(" ");
println!("Bot spreads: {spread}");
}
_ => println!("Bot discards {}.", top.expect("a turn ends with a discard")),
}
}
Phase::Layoff => {
let laid = round.laid_off();
if round.phase() == Phase::Layoff {
println!("Bot lays off (so far: {}).", list(laid));
} else if laid.is_empty() {
println!("Bot lays nothing off.");
} else {
println!("Bot lays off {}.", list(laid));
}
}
Phase::Finished => {}
}
}
fn describe(result: RoundResult, rules: &Rules) -> String {
let pts = result.points(rules);
match result {
RoundResult::Dead => "dead hand, nobody scores".into(),
RoundResult::Knock { winner, margin } => format!("{} knock for {margin}", who(winner)),
RoundResult::Undercut { winner, margin } => {
format!(
"{} undercut ({margin} + {} = {pts})",
who(winner),
pts - u16::from(margin)
)
}
RoundResult::Gin { winner, deadwood } => {
format!(
"{} gin ({deadwood} + {} = {pts})",
who(winner),
pts - u16::from(deadwood)
)
}
RoundResult::BigGin { winner, deadwood } => {
format!(
"{} BIG gin ({deadwood} + {} = {pts})",
who(winner),
pts - u16::from(deadwood)
)
}
_ => format!("{result:?}"),
}
}
fn who(player: Player) -> &'static str {
if player == HUMAN { "You" } else { "Bot" }
}
fn main() -> Result<()> {
let (spec, rules, seed) = parse_args()?;
let mut rng = match seed {
Some(seed) => StdRng::seed_from_u64(seed),
None => StdRng::from_rng(&mut rand::rng()),
};
let mut bot = make_bot(&spec, &mut rng)?;
let mut human = HumanCli {
by_suit: false,
predraw: None,
hinter: MonteCarloBot::new(StdRng::seed_from_u64(seed.unwrap_or(0))),
};
let mut game = gin_rummy::Game::new(rules, HUMAN);
println!("Gin rummy to {} points — you vs {spec}.", rules.game_target);
for number in 1.. {
if game.is_over() {
break;
}
println!();
println!(
"=== Round {number} ({} deal) ===",
if game.next_dealer() == HUMAN {
"your"
} else {
"bot's"
},
);
let scores = [game.score(Player::One), game.score(Player::Two)];
let mut table = Table::new(game.deal(&mut rng)).scores(scores);
while let Some(seat) = table.turn() {
if seat == HUMAN {
if let Err(EngineError::IllegalAction { source, .. }) = table.step(&mut human) {
println!("Illegal: {source}");
}
} else {
let phase = table.round().phase();
let top = table.round().discard_pile().last().copied();
let pile = table.round().discard_pile().len();
table
.step(&mut *bot)
.expect("the bot always chooses legal actions");
narrate_bot(phase, top, pile, &table);
}
}
let result = table.round().result().expect("a turnless round finished");
game.record(result)?;
println!();
println!("Round {number}: {}", describe(result, &rules));
println!(
"Score — you {} : {} bot",
game.score(HUMAN),
game.score(HUMAN.opponent()),
);
}
let settled = game.final_score().expect("the game just ended");
println!();
println!(
"{} the game, {} : {}{}",
if settled.winner == HUMAN {
"You win"
} else {
"Bot wins"
},
settled.totals[settled.winner as usize],
settled.totals[settled.winner.opponent() as usize],
if settled.shutout {
" — a shutout!"
} else {
""
},
);
Ok(())
}