Files
Ferrous-Solitaire/solitaire_assetgen/src/bin/gen_seeds.rs
T
funman300 5c992cbdca refactor: replace local DrawMode with upstream klondike::DrawStockConfig (#82)
DrawMode was a 1:1 mirror of klondike::DrawStockConfig (DrawOne/DrawThree).
Delete it and use the upstream type everywhere; re-export DrawStockConfig from
solitaire_core. config_for assigns draw_stock directly and draw_mode() returns
session.config().inner.draw_stock.

Serde is unchanged — DrawStockConfig serialises to the same "DrawOne"/"DrawThree"
named variants, so persisted game_state.json / replay JSON stay byte-compatible
(no migration). Field/method/variable names containing draw_mode are unchanged.

35 files, mechanical type swap across all crates. Implemented via a multi-agent
workflow (core → per-crate consumers → verify). cargo test --workspace and
clippy --workspace --all-targets -- -D warnings green.

Closes #82

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:01:11 -07:00

186 lines
5.4 KiB
Rust

//! Generate provably-winnable Klondike seeds for `CHALLENGE_SEEDS`.
//!
//! Walks seeds incrementally from `--start`, calls the solver on each, and
//! collects only those proven winnable (`Ok(Some(_))`; inconclusive is
//! rejected — the curated list wants proof). Prints Rust source suitable for
//! pasting into `solitaire_data/src/challenge.rs`.
//!
//! # Usage
//!
//! ```bash
//! cargo run -p solitaire_assetgen --bin gen_seeds --release -- \
//! --start 0xCAFE_BABE_0000_0000 --count 75
//! ```
//!
//! Flags:
//! --start Starting seed (decimal or 0x-prefixed hex, default 0xCAFEBABE00000000)
//! --count Number of Winnable seeds to emit (default 75)
//! --help Print this message
use solitaire_core::DrawStockConfig;
use solitaire_core::game_state::GameState;
use solitaire_core::{DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET};
fn main() {
let mut args = std::env::args().skip(1).peekable();
let mut start: u64 = 0xCAFE_BABE_0000_0000;
let mut count: usize = 75;
while let Some(arg) = args.next() {
match arg.as_str() {
"--start" => {
let val = args.next().unwrap_or_else(|| {
eprintln!("error: --start requires a value");
std::process::exit(1);
});
start = parse_u64(&val);
}
"--count" => {
let val = args.next().unwrap_or_else(|| {
eprintln!("error: --count requires a value");
std::process::exit(1);
});
count = val.parse().unwrap_or_else(|_| {
eprintln!("error: --count must be a positive integer");
std::process::exit(1);
});
}
"--help" | "-h" => {
eprintln!(
"{}",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/bin/gen_seeds.rs"))
.lines()
.take(20)
.collect::<Vec<_>>()
.join("\n")
);
return;
}
other => {
eprintln!("error: unknown argument: {other}");
std::process::exit(1);
}
}
}
if count == 0 {
eprintln!("error: --count must be > 0");
std::process::exit(1);
}
let draw_mode = DrawStockConfig::DrawOne;
let mut found: Vec<u64> = Vec::with_capacity(count);
let mut tried: u64 = 0;
let mut seed = start;
eprintln!("gen_seeds: finding {count} Winnable seeds from 0x{start:016X} (DrawOne) …");
while found.len() < count {
tried += 1;
if matches!(
GameState::solve_fresh_deal(
seed,
draw_mode,
DEFAULT_SOLVE_MOVES_BUDGET,
DEFAULT_SOLVE_STATES_BUDGET
),
Ok(Some(_))
) {
found.push(seed);
eprintln!(
" [{:>3}/{}] 0x{:016X} ({} tried so far)",
found.len(),
count,
seed,
tried
);
}
seed = seed.wrapping_add(1);
}
eprintln!(
"\nDone. Paste the block below into CHALLENGE_SEEDS in solitaire_data/src/challenge.rs:\n"
);
println!(
" // Generated by solitaire_assetgen::gen_seeds \
(start=0x{start:016X}, count={count}, date={date})",
date = current_date()
);
for chunk in found.chunks(5) {
for s in chunk {
println!(
" 0x{:04X}_{:04X}_{:04X}_{:04X},",
(s >> 48) & 0xFFFF,
(s >> 32) & 0xFFFF,
(s >> 16) & 0xFFFF,
s & 0xFFFF,
);
}
println!();
}
}
fn parse_u64(s: &str) -> u64 {
let cleaned = s.replace('_', "");
if let Some(hex) = cleaned
.strip_prefix("0x")
.or_else(|| cleaned.strip_prefix("0X"))
{
u64::from_str_radix(hex, 16).unwrap_or_else(|_| {
eprintln!("error: could not parse '{s}' as a hex u64");
std::process::exit(1);
})
} else {
cleaned.parse().unwrap_or_else(|_| {
eprintln!("error: could not parse '{s}' as a decimal u64");
std::process::exit(1);
})
}
}
fn current_date() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let days = secs / 86400;
// Gregorian calendar computation (Tomohiko Sakamoto's algorithm variant)
let mut y = 1970u64;
let mut d = days;
loop {
let leap = (y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400);
let days_in_year = if leap { 366 } else { 365 };
if d < days_in_year {
break;
}
d -= days_in_year;
y += 1;
}
let leap = (y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400);
let month_days: [u64; 12] = [
31,
if leap { 29 } else { 28 },
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
];
let mut m = 0usize;
for &md in &month_days {
if d < md {
break;
}
d -= md;
m += 1;
}
format!("{y}-{:02}-{:02}", m + 1, d + 1)
}