144 lines
5 KiB
Rust
144 lines
5 KiB
Rust
|
|
//! F17's reproduction: what is ATTACK worth? (CB-WP-0029 follow-up.)
|
||
|
|
//!
|
||
|
|
//! The maintainer reported *"there is no incentive to play attacks as long
|
||
|
|
//! as I have positive cards"*. F17 sat as a note because nothing
|
||
|
|
//! demonstrated it. This is the artifact.
|
||
|
|
//!
|
||
|
|
//! GreedyPolicy ranks Attack at 10, below everything (bot.rs). So its zero
|
||
|
|
//! attacks measure OUR HEURISTIC, not the game. The test is a policy
|
||
|
|
//! identical in every other respect with Attack promoted: if it wins as
|
||
|
|
//! often, attacking is neutral; much less, avoiding it is correct play;
|
||
|
|
//! more, and greedy is simply wrong.
|
||
|
|
use cb_game_runtime::{ScenarioGame, Setup};
|
||
|
|
use cb_kernel::PlayerId;
|
||
|
|
use games_ground::bot::{play, Choice, GreedyPolicy, Policy};
|
||
|
|
use games_ground::{Action, GroundCommand, GroundState};
|
||
|
|
|
||
|
|
/// Greedy's ordering with ATTACK promoted to the top of the action list.
|
||
|
|
/// A copy of the ranking, not a call into it: `GreedyPolicy::rank` is
|
||
|
|
/// private, and the point is to vary exactly one number.
|
||
|
|
struct Attacker(i32);
|
||
|
|
impl Policy for Attacker {
|
||
|
|
fn name(&self) -> &'static str {
|
||
|
|
"attacker"
|
||
|
|
}
|
||
|
|
fn choose(
|
||
|
|
&mut self,
|
||
|
|
state: &GroundState,
|
||
|
|
seat: PlayerId,
|
||
|
|
legal: &[GroundCommand],
|
||
|
|
_p: bool,
|
||
|
|
) -> Choice {
|
||
|
|
let gated = state
|
||
|
|
.players
|
||
|
|
.get(&seat)
|
||
|
|
.is_some_and(|p| p.stress >= 4 && !p.freedom_gate_lifted);
|
||
|
|
let rank = |c: &GroundCommand| -> i32 {
|
||
|
|
match c {
|
||
|
|
GroundCommand::SelectAction {
|
||
|
|
action, problem, ..
|
||
|
|
} => match action {
|
||
|
|
Action::Ground if gated => 100,
|
||
|
|
Action::Attack => self.0, // <-- the ONE varying number (greedy: 10)
|
||
|
|
Action::Solve
|
||
|
|
if problem
|
||
|
|
.and_then(|p| state.problems.get(&p))
|
||
|
|
.is_some_and(|p| p.claimed_by.is_some()) =>
|
||
|
|
{
|
||
|
|
5
|
||
|
|
}
|
||
|
|
Action::Solve => 90,
|
||
|
|
Action::Investigate => 80,
|
||
|
|
Action::Support => 70,
|
||
|
|
Action::Ground => 60,
|
||
|
|
},
|
||
|
|
GroundCommand::SpendFreedom => {
|
||
|
|
if gated {
|
||
|
|
96
|
||
|
|
} else {
|
||
|
|
0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
GroundCommand::Reveal | GroundCommand::Resolve | GroundCommand::EndRound => -1,
|
||
|
|
_ => 50,
|
||
|
|
}
|
||
|
|
};
|
||
|
|
let mut best = 0usize;
|
||
|
|
for (i, c) in legal.iter().enumerate() {
|
||
|
|
if rank(c) > rank(&legal[best]) {
|
||
|
|
best = i;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Choice::Command(best)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn sweep(
|
||
|
|
name: &str,
|
||
|
|
players: u8,
|
||
|
|
mk: &dyn Fn(u64, u8) -> Vec<Box<dyn Policy>>,
|
||
|
|
) -> (u32, u32, u32, u32) {
|
||
|
|
let (mut games, mut won, mut atk, mut darvo) = (0, 0, 0, 0);
|
||
|
|
for seed in 0..200u64 {
|
||
|
|
let Ok(st) = GroundState::setup(
|
||
|
|
&Setup {
|
||
|
|
players,
|
||
|
|
preset: format!("standard-{players}p"),
|
||
|
|
patch: Default::default(),
|
||
|
|
},
|
||
|
|
seed,
|
||
|
|
) else {
|
||
|
|
continue;
|
||
|
|
};
|
||
|
|
let mut ps = mk(seed, players);
|
||
|
|
let Ok(g) = play(st, &mut ps) else { continue };
|
||
|
|
games += 1;
|
||
|
|
if g.state.outcome.as_ref().is_some_and(|o| o.group_success) {
|
||
|
|
won += 1;
|
||
|
|
}
|
||
|
|
for (_, c) in &g.steps {
|
||
|
|
if let GroundCommand::SelectAction {
|
||
|
|
action: Action::Attack,
|
||
|
|
..
|
||
|
|
} = c
|
||
|
|
{
|
||
|
|
atk += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for e in &g.events {
|
||
|
|
if matches!(e, games_ground::GroundEvent::DarvoTriggered { .. }) {
|
||
|
|
darvo += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let _ = name;
|
||
|
|
(games, won, atk, darvo)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn main() {
|
||
|
|
println!("F17: does ATTACK cost you the game, or does our bot just avoid it?\n");
|
||
|
|
// The gradient, not just the extremes: greedy ranks Attack at 10,
|
||
|
|
// below everything. 75 puts it above SUPPORT and below INVESTIGATE --
|
||
|
|
// "attack when convenient". 95 is "attack whenever legal".
|
||
|
|
println!(" rank=10 (greedy) rank=75 (sometimes) rank=95 (always)");
|
||
|
|
println!("seats won atk darvo won atk darvo won atk darvo");
|
||
|
|
for players in [2u8, 3, 4, 5, 6] {
|
||
|
|
let cell = |r: i32, p: u8| {
|
||
|
|
sweep("x", p, &move |_s, n| {
|
||
|
|
(0..n)
|
||
|
|
.map(|_| Box::new(Attacker(r)) as Box<dyn Policy>)
|
||
|
|
.collect()
|
||
|
|
})
|
||
|
|
};
|
||
|
|
let (_, gw, ga, gd) = sweep("greedy", players, &|_s, p| {
|
||
|
|
(0..p)
|
||
|
|
.map(|_| Box::new(GreedyPolicy) as Box<dyn Policy>)
|
||
|
|
.collect()
|
||
|
|
});
|
||
|
|
let (_, mw, ma, md) = cell(75, players);
|
||
|
|
let (_, aw, aa, ad) = cell(95, players);
|
||
|
|
println!(" {players}p {gw:>4} {ga:>4} {gd:>5} {mw:>4} {ma:>4} {md:>5} {aw:>4} {aa:>4} {ad:>5}");
|
||
|
|
}
|
||
|
|
println!("\n(200 games per cell; greedy shown as the rank=10 column)");
|
||
|
|
}
|