docs(core): record unlimited stock recycling as an intentional rules decision
Resolves the Draw-1 recycle question from the June 500-game audit: unlimited recycling with upstream score penalties is deliberate, matching mainstream digital solitaire rather than strict 3-pass tournament rules. The difficulty seed catalog and the winnable-deal solver are verified under this rule, so a hard pass limit must not be introduced casually. - ARCHITECTURE.md: new 'Rules decisions' note in the solitaire_core section - GameState::draw() doc comment points at the decision record - New lock-in test draw_one_recycling_is_unlimited_by_design asserts 10+ recycles are never rejected Closes #117 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -109,6 +109,18 @@ Owns:
|
||||
- Achievement unlock condition evaluation
|
||||
- Seeded RNG for reproducible deals
|
||||
|
||||
**Rules decisions:**
|
||||
- **Stock recycling is unlimited in every draw mode — by design.** Extra
|
||||
passes through the stock are discouraged via the upstream score penalty
|
||||
(applied by the `card_game`/`klondike` session), never blocked with a
|
||||
`MoveError`. This matches mainstream digital solitaire (unlimited redeals
|
||||
in Draw-1) rather than strict tournament rules (3-pass cap). It is load-
|
||||
bearing: the difficulty seed catalog and the winnable-deal solver are
|
||||
verified under unlimited recycling, so introducing a hard pass limit would
|
||||
invalidate both. Locked in by the
|
||||
`draw_one_recycling_is_unlimited_by_design` test in
|
||||
`solitaire_core/src/game_state.rs`. (Decision record: Gitea issue #117.)
|
||||
|
||||
### `solitaire_sync`
|
||||
**Dependencies:** `serde`, `serde_json`, `uuid`, `chrono` only.
|
||||
|
||||
|
||||
@@ -8,6 +8,12 @@ project follows [Semantic Versioning](https://semver.org/).
|
||||
|
||||
### Added
|
||||
|
||||
- **Rules decision record: unlimited stock recycling.** Documented in
|
||||
`ARCHITECTURE.md` that unlimited recycling with score penalties (matching
|
||||
mainstream digital solitaire) is intentional, and locked it in with a core
|
||||
test — the difficulty seed catalog and winnable-deal solver are verified
|
||||
under this rule. Resolves the last open finding from the June 500-game
|
||||
audit (issue #117).
|
||||
- **Analytics validation runbook.** Documented native Matomo live validation,
|
||||
expected event payloads, and the current web/WASM analytics split.
|
||||
- **Android smoke-test runbook.** Updated the Android doc with the current
|
||||
|
||||
@@ -863,6 +863,13 @@ impl GameState {
|
||||
}
|
||||
|
||||
/// Draw cards from stock to waste. When stock is empty, recycles waste back to stock.
|
||||
///
|
||||
/// Recycling is deliberately unlimited in every draw mode: extra passes
|
||||
/// are discouraged through the upstream score penalty, never rejected
|
||||
/// with a [`MoveError`]. The difficulty seed catalog and the
|
||||
/// winnable-deal solver are verified under this rule — see the
|
||||
/// "Rules decisions" note in `ARCHITECTURE.md` (`solitaire_core`
|
||||
/// section) before considering a hard pass limit.
|
||||
pub fn draw(&mut self) -> Result<(), MoveError> {
|
||||
if self.is_won() {
|
||||
return Err(MoveError::GameAlreadyWon);
|
||||
@@ -1222,6 +1229,30 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_one_recycling_is_unlimited_by_design() {
|
||||
// Rules decision (Gitea #117): stock recycling is unlimited in every
|
||||
// draw mode. Extra passes are discouraged via the upstream score
|
||||
// penalty, never blocked with a MoveError. The difficulty seed
|
||||
// catalog and the winnable-deal solver are verified under this rule,
|
||||
// so a hard pass limit must not be introduced casually — see the
|
||||
// "Rules decisions" note in ARCHITECTURE.md (`solitaire_core`).
|
||||
let mut game = game_at_first_recycle().expect("could not reach recycle");
|
||||
// ~24 draws per Draw-1 pass; 2_000 draws is ample budget for 10 passes.
|
||||
for _ in 0..2_000 {
|
||||
if game.recycle_count() >= 10 {
|
||||
break;
|
||||
}
|
||||
game.draw()
|
||||
.expect("unlimited recycling: draw must never fail on pass count");
|
||||
}
|
||||
assert!(
|
||||
game.recycle_count() >= 10,
|
||||
"expected at least 10 recycles within the draw budget, got {}",
|
||||
game.recycle_count(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undo_applies_minus_15_penalty_via_upstream_score() {
|
||||
// A foundation move scores +10 upstream; undoing it nets the move score
|
||||
|
||||
Reference in New Issue
Block a user