//! Generate provably-winnable Klondike seeds for `CHALLENGE_SEEDS`. //! //! Walks seeds incrementally from `--start`, calls the solver on each, and //! collects only those that return `SolverResult::Winnable` (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::game_state::DrawMode; use solitaire_core::solver::{try_solve, SolverConfig, SolverResult}; 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::>().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 cfg = SolverConfig::default(); let draw_mode = DrawMode::DrawOne; let mut found: Vec = 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!( try_solve(seed, draw_mode.clone(), &cfg), SolverResult::Winnable ) { 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) }