6e407a3ea7
Build and Deploy / build-and-push (push) Successful in 3m54s
- #66: Clamp safe-area insets to 25% of window height with warn!() on excess - #68: Move fire_flush outside per-event loop in analytics (batch flush once) - #56: Persist progress before marking reward_granted to prevent XP loss on crash - #60: Add DateRolloverTimer + check_date_rollover system for midnight seed refresh - #62: Add validate_header() in replay upload with mode/draw_mode allowlists - #61: Restore two-query leaderboard opt-in check (SELECT then UPDATE); original queries already in .sqlx cache; EXISTS variant would require sqlx prepare Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
178 lines
5.2 KiB
Rust
178 lines
5.2 KiB
Rust
//! 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::{SolverConfig, SolverResult, try_solve};
|
|
|
|
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 cfg = SolverConfig::default();
|
|
let draw_mode = DrawMode::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!(try_solve(seed, draw_mode, &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)
|
|
}
|