docs(core): record unlimited stock recycling as an intentional rules decision #122
@@ -109,6 +109,18 @@ Owns:
|
|||||||
- Achievement unlock condition evaluation
|
- Achievement unlock condition evaluation
|
||||||
- Seeded RNG for reproducible deals
|
- 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`
|
### `solitaire_sync`
|
||||||
**Dependencies:** `serde`, `serde_json`, `uuid`, `chrono` only.
|
**Dependencies:** `serde`, `serde_json`, `uuid`, `chrono` only.
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ project follows [Semantic Versioning](https://semver.org/).
|
|||||||
|
|
||||||
### Added
|
### 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,
|
- **Analytics validation runbook.** Documented native Matomo live validation,
|
||||||
expected event payloads, and the current web/WASM analytics split.
|
expected event payloads, and the current web/WASM analytics split.
|
||||||
- **Android smoke-test runbook.** Updated the Android doc with the current
|
- **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.
|
/// 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> {
|
pub fn draw(&mut self) -> Result<(), MoveError> {
|
||||||
if self.is_won() {
|
if self.is_won() {
|
||||||
return Err(MoveError::GameAlreadyWon);
|
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]
|
#[test]
|
||||||
fn undo_applies_minus_15_penalty_via_upstream_score() {
|
fn undo_applies_minus_15_penalty_via_upstream_score() {
|
||||||
// A foundation move scores +10 upstream; undoing it nets the move score
|
// A foundation move scores +10 upstream; undoing it nets the move score
|
||||||
|
|||||||
Reference in New Issue
Block a user