From 2062bd06f321c9167f9fa7acbf7392c67417733f Mon Sep 17 00:00:00 2001 From: funman300 Date: Fri, 8 May 2026 20:19:11 -0700 Subject: [PATCH] feat(data): expand challenge seed pool with 75 verified wins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a gen_seeds binary to solitaire_assetgen that brute-searches seeds for hands solvable in ≤250 moves, then writes the list. The 75 new seeds (0xCAFEBABE prefix) are appended to CHALLENGE_SEEDS in solitaire_data::challenge. Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 2 + solitaire_assetgen/Cargo.toml | 6 + solitaire_assetgen/src/bin/gen_seeds.rs | 157 ++++++++++++++++++++++++ solitaire_data/src/challenge.rs | 76 ++++++++++++ 4 files changed, 241 insertions(+) create mode 100644 solitaire_assetgen/src/bin/gen_seeds.rs diff --git a/Cargo.lock b/Cargo.lock index 589aca7..32cb155 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6967,6 +6967,8 @@ version = "0.1.0" dependencies = [ "ab_glyph", "png 0.17.16", + "solitaire_core", + "solitaire_data", ] [[package]] diff --git a/solitaire_assetgen/Cargo.toml b/solitaire_assetgen/Cargo.toml index 60a3d81..70be544 100644 --- a/solitaire_assetgen/Cargo.toml +++ b/solitaire_assetgen/Cargo.toml @@ -12,6 +12,8 @@ publish = false [dependencies] png = "0.17" ab_glyph = "0.2" +solitaire_core = { path = "../solitaire_core" } +solitaire_data = { path = "../solitaire_data" } [[bin]] name = "gen_sfx" @@ -20,3 +22,7 @@ path = "src/bin/gen_sfx.rs" [[bin]] name = "gen_art" path = "src/bin/gen_art.rs" + +[[bin]] +name = "gen_seeds" +path = "src/bin/gen_seeds.rs" diff --git a/solitaire_assetgen/src/bin/gen_seeds.rs b/solitaire_assetgen/src/bin/gen_seeds.rs new file mode 100644 index 0000000..0c0441f --- /dev/null +++ b/solitaire_assetgen/src/bin/gen_seeds.rs @@ -0,0 +1,157 @@ +//! 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) +} diff --git a/solitaire_data/src/challenge.rs b/solitaire_data/src/challenge.rs index f5602ca..8fda817 100644 --- a/solitaire_data/src/challenge.rs +++ b/solitaire_data/src/challenge.rs @@ -40,6 +40,82 @@ pub const CHALLENGE_SEEDS: &[u64] = &[ 0xDDDD_EEEE_FFFF_0000, 0x0101_0101_0101_0101, 0xA1B2_C3D4_E5F6_0718, + // Generated by solitaire_assetgen::gen_seeds (start=0xCAFEBABE00000000, count=75, date=2026-05-09) + 0xCAFE_BABE_0000_0000, + 0xCAFE_BABE_0000_0002, + 0xCAFE_BABE_0000_0004, + 0xCAFE_BABE_0000_0008, + 0xCAFE_BABE_0000_000B, + 0xCAFE_BABE_0000_000D, + 0xCAFE_BABE_0000_000E, + 0xCAFE_BABE_0000_0010, + 0xCAFE_BABE_0000_0011, + 0xCAFE_BABE_0000_0014, + 0xCAFE_BABE_0000_0016, + 0xCAFE_BABE_0000_0019, + 0xCAFE_BABE_0000_001A, + 0xCAFE_BABE_0000_001F, + 0xCAFE_BABE_0000_0020, + 0xCAFE_BABE_0000_0021, + 0xCAFE_BABE_0000_0024, + 0xCAFE_BABE_0000_0025, + 0xCAFE_BABE_0000_0027, + 0xCAFE_BABE_0000_002B, + 0xCAFE_BABE_0000_002D, + 0xCAFE_BABE_0000_0030, + 0xCAFE_BABE_0000_0034, + 0xCAFE_BABE_0000_0036, + 0xCAFE_BABE_0000_003A, + 0xCAFE_BABE_0000_003B, + 0xCAFE_BABE_0000_003D, + 0xCAFE_BABE_0000_0042, + 0xCAFE_BABE_0000_0043, + 0xCAFE_BABE_0000_0044, + 0xCAFE_BABE_0000_004C, + 0xCAFE_BABE_0000_004D, + 0xCAFE_BABE_0000_004F, + 0xCAFE_BABE_0000_0050, + 0xCAFE_BABE_0000_0051, + 0xCAFE_BABE_0000_0054, + 0xCAFE_BABE_0000_0055, + 0xCAFE_BABE_0000_0056, + 0xCAFE_BABE_0000_0059, + 0xCAFE_BABE_0000_005B, + 0xCAFE_BABE_0000_005C, + 0xCAFE_BABE_0000_005E, + 0xCAFE_BABE_0000_0060, + 0xCAFE_BABE_0000_0062, + 0xCAFE_BABE_0000_0064, + 0xCAFE_BABE_0000_0067, + 0xCAFE_BABE_0000_0069, + 0xCAFE_BABE_0000_006A, + 0xCAFE_BABE_0000_006B, + 0xCAFE_BABE_0000_006C, + 0xCAFE_BABE_0000_006D, + 0xCAFE_BABE_0000_006E, + 0xCAFE_BABE_0000_006F, + 0xCAFE_BABE_0000_0072, + 0xCAFE_BABE_0000_0073, + 0xCAFE_BABE_0000_0074, + 0xCAFE_BABE_0000_0079, + 0xCAFE_BABE_0000_007A, + 0xCAFE_BABE_0000_007D, + 0xCAFE_BABE_0000_007E, + 0xCAFE_BABE_0000_007F, + 0xCAFE_BABE_0000_0082, + 0xCAFE_BABE_0000_0083, + 0xCAFE_BABE_0000_0084, + 0xCAFE_BABE_0000_0085, + 0xCAFE_BABE_0000_0089, + 0xCAFE_BABE_0000_008A, + 0xCAFE_BABE_0000_008D, + 0xCAFE_BABE_0000_008E, + 0xCAFE_BABE_0000_0090, + 0xCAFE_BABE_0000_0094, + 0xCAFE_BABE_0000_0095, + 0xCAFE_BABE_0000_0098, + 0xCAFE_BABE_0000_0099, + 0xCAFE_BABE_0000_009F, ]; /// Resolve a `challenge_index` to its corresponding seed, wrapping when