Task List
This file is the living implementation checklist for building the game. Tasks are grouped by phase from the Roadmap. Within each phase, the order is the execution order.
- Work top-to-bottom within the active phase.
- A checkbox should be large enough for one focused implementation session. Small adjacent changes belong in the same task.
- Use plain bullets under a task for acceptance notes and scope details. Do not add checkboxes to non-tasks.
- A task is done when its scope notes are true and the change is merged
to
main.
Phase 0 — Project setup
Goal: love . opens a window. CI is green on macOS and Linux. The
plumbing every later phase relies on (i18n, settings, save dir) exists
from day one.
- Initialise the Love2D project skeleton and repository layout.
main.lua,conf.lua, window title, and Love2D version are set.- Top-level directories match Architecture:
core/,app/,ui/,assets/,tests/,platform/.
- Add Lua formatting, linting, tests, and local command entry points.
styluaandluacheckshare project-level config.- Plain-
luatests run from the command line through a single script ormake test.
- Add GitHub Actions for linting and tests on every push.
- Add i18n plumbing from day one.
t(key, ...)reads fromassets/i18n/<locale>.lua.- Active locale lives in settings.
- Stub locale tables exist for
en,ru,pl, anduk. - Missing active-locale keys fall back to
enand log once per key.
- Document how to run, test, and package the project in the root
README.md. - Produce a
.loveartifact on every push tomain. - Add local pre-commit checks for linting and tests.
- Add a CI guard for hard-coded player-visible strings outside locale tables.
- Set up the e2e test harness for Phase 2+ UI work.
- Pure-Lua mock of the
loveglobal so journey tests run under busted with no Love2D runtime. - Driver loads
main.luaagainst the mock and exposes step / click / keypress / resize, plus a localised-string finder. - Recording captures
clear,print,printf,setColor,rectangle, andpush/pop/translateso journeys can assert what was drawn. - First sanity-check journey passes against the placeholder
main.luaand is ready to extend when the scene skeleton lands. - Busted picks up
tests/e2e/automatically and the 80%-of-core/coverage gate stays untouched.
- Pure-Lua mock of the
Phase 1 — Core rules engine (pure Lua)
Goal: every rule from Rules of Play implemented and tested. No Love2D in this layer.
RuleConfig from day oneThe engine is parameterised by a RuleConfig from this phase's first
commit. Phase 1 ships exactly one RuleConfig value — the canonical
Russian default — but never hard-codes a value any future variant could
change (talon size, bid increments, marriage values, barrel rules, ...).
1.1 Cards & deck
- Define the canonical
RuleConfigbaseline and the card model.- Card has suit (♠♣♦♥) and rank (9,J,Q,K,10,A).
- Card point values are A=11, 10=10, K=4, Q=3, J=2, 9=0.
- Trick rank order is A > 10 > K > Q > J > 9.
- These values are read from the canonical Phase 1
RuleConfig, not duplicated as unrelated constants.
- Build the deterministic 24-card deck module.
- Deck builder returns exactly one card for every suit/rank pair.
- Total deck card points equal 120.
- Shuffle is seedable and reproducible in tests.
1.2 Dealing
- Implement standard 3-player deal and deal validation.
- Deal sequence is 3+3+3, 2 to talon, 2+2, 1 to talon, 2+2.
- Each player ends with 7 cards and the talon has 3 cards.
- Every completed deal still contains exactly 24 unique cards.
- Misdeals such as wrong count or exposed card return typed errors.
1.3 Auction / bidding
- Implement the auction state machine.
- Auction starts at forehand and advances clockwise.
- Opening minimum is 100; pre-talon maximum is 120.
- Bid increments are 5 below 200 and 10 from 200 onward.
- Pass is permanent.
- Auction ends when two players have passed.
- Declarer is the last remaining bidder at their final bid.
- Illegal bid attempts return typed errors.
1.4 Talon, pass and raise
- Implement talon reveal, pickup, pass, and post-talon raise.
- The 3 talon cards are publicly revealed.
- Declarer takes the talon and reaches 10 cards; opponents stay at 7.
- Declarer passes 1 face-down card to each opponent, producing 8 / 8 / 8.
- Declarer may raise after seeing the talon but cannot lower the bid.
- Illegal raises, including wrong increment and below-current-bid values, return typed errors.
1.5 Marriages and trump
- Implement marriage detection, declaration, bonuses, and trump state.
- A marriage is K + Q of the same suit in one hand.
- Marriage values are ♥=100, ♦=80, ♣=60, ♠=40.
- A marriage is declared by leading the K or Q while on lead.
- The bonus posts immediately.
- No trump exists until the first declared marriage.
- Declared marriage suit becomes trump from the next trick.
- Multiple marriages are allowed in one deal and each replaces trump.
- A marriage formed through the talon is legal.
1.6 Trick-taking
- Implement legal-play validation and trick resolution.
- Player must follow suit if possible.
- Player must beat the led card if possible when following suit.
- Player must trump when void in led suit and trump exists.
- Player must overtrump if possible.
- Player may discard freely only when holding neither led suit nor trump.
- Trick winner is highest trump, otherwise highest card of led suit.
- Trick winner leads next.
- A deal is exactly 8 tricks.
- Every illegal play is logged with the rule it broke.
- Fix trick-count inconsistency in
docs/rules/setup.md.- The overview at line 21 says "7 tricks played"; every other rules page and this task list establish 8 tricks per deal (each player holds 8 cards after the talon pass).
1.7 Scoring & barrel
- Implement deal scoring and contract resolution.
- Captured card points sum to at most 120 across all sides.
- Marriage bonuses are credited to the player who declared them.
- Captured card points round to nearest 5; marriage bonuses stay exact.
- Declarer made contract when deal score is at least the bid.
- Successful declarer adds the bid to running total.
- Failed declarer subtracts the bid from running total.
- Defenders independently add their captured deal scores.
- Implement barrel and game-end scoring.
- At 880, score freezes and the player has 3 deals to make 120 and win.
- Falling off the barrel applies −120 and returns the player to 760.
- Reaching 1000 wins.
- Barrel collision rule is last-mounter survives; others fall off.
- If multiple players cross 1000 in the same deal, declarer wins ties.
1.8 Engine tests
- Build the core rules test suite.
- Every public function in
core/has unit coverage. - A scripted full deal asserts final scores.
- Public APIs cannot reach a rule-violating state in scripted or property-style tests.
- Coverage report exists with a target of at least 80% for
core/.
- Every public function in
Phase 2 — Hot-seat MVP
Goal: three humans pass a desktop around and play a real game to 1000. The first table UI is already touch-ready and reflowable so iOS is a porting pass later, not a UI rewrite.
- Build the scene skeleton and game navigation.
- Main menu, table, and end-of-game scenes exist.
- New-game and abandon-game controls are available from the menu.
- Buttons render hover, focus, pressed, and disabled visual states; menus are fully keyboard-navigable (Tab/arrows + Enter/Space + Esc).
- Table scene exposes a visible
Menubutton so the back-out path works on touch devices, not just via Esc.
- Build touch-ready input and reflowable table foundations.
- Mouse and touch use the same action paths.
- No required interaction depends on hover.
- Primary card/table controls are sized for finger input from the first UI pass.
- Table layout responds to window size instead of using fixed desktop-only coordinates.
- Render the playable table state with placeholder assets.
- 24-card deck, player hand, talon, current turn, current bid, running scoreboard, and end-of-game winner/final scores are visible.
- Connect hot-seat input to the rules engine.
- Player can click or tap cards through hit-tests.
- Auction UI lets each player bid or pass in turn.
- Talon reveal and pass-card interactions are playable.
- Marriage declaration is available when leading a K or Q from a held marriage.
- Add hot-seat privacy and hand-off flow.
- A pass-to-next-player overlay hides inactive hands.
- Each player only sees their own hand during private decisions.
- Add a Settings scene with a hot-seat privacy toggle.
app/settings.luapersists user preferences tosettings.jsonvialove.filesystem, validated byschemaVersion.app/json.luaprovides the minimal encoder/decoder reused by Phase 2 auto-save and Phase 3 templates.- Settings scene exposes one row + Back; Phase 5's full settings screen extends this scene with locale, sound, theme, and animation speed.
- Disabling the toggle suppresses the privacy curtain entirely so a single tester can drive every seat without dismissing between turns.
- Add legal-action affordances.
- Cards that are legal under must-follow, must-beat, and must-trump rules can be visually distinguished.
- Illegal player actions are blocked with localised feedback.
- Route every player-visible string in the hot-seat MVP through
t().- This includes placeholder UI, error messages, button labels, and end-of-game text.
- Add baseline auto-save and restore.
- One auto-save slot writes on app suspend, graceful quit, and after every scored deal.
- Save format is JSON via
love.filesystem. - Save includes
schemaVersion, running scores, hands, talon, bids, played tricks, declared marriages, and current trump. - Next launch restores the auto-save if present.
Phase 3 — Rule template system
Goal: every variant from Variations is
represented as data in a single RuleConfig system and the engine
honours every selectable toggle. Each documented toggle is either
implemented and selectable, or explicitly marked deferred so built-in
templates and saved custom templates cannot accidentally depend on it.
3.1 RuleConfig model & engine wiring
- Expand
RuleConfiginto the single schema for every engine toggle.- Every field has a type, allowed values, default value, and
schemaVersionhandling. - Every field records whether it is implemented, selectable, or deferred.
RuleConfiground-trips through JSON for persistence and saved games.- Incompatible combinations are rejected with clear, localisable errors.
- Every field has a type, allowed values, default value, and
- Refactor the core engine so every variable rule reads from
RuleConfig.- No hard-coded constants remain for rules that variants can change.
- Engine tests pass under every built-in template added in this phase.
3.2 Toggle catalogue
Group toggles in RuleConfig so the UI can render clear sections. Each
toggle's wording maps to House Rules,
which remains the source of truth for behavior. Catalogue tasks mean
schema, validation, and UI representation; gameplay behavior is required
only for toggles marked selectable.
- Add players and seating toggles.
- Player count (2 / 3 / 4).
- Partnership mode (none / fixed across-the-table), valid only for 4 players.
- 4-player configuration (dealer plays no talon / dealer sits out).
- 2-player configuration (closed-talon draw stock / fixed deal no draw).
- Add dealing and redeal-trigger toggles.
- 4-nine redeal (off / optional / mandatory).
- 3-nine redeal (off / optional).
- 4-jack redeal (off / on).
- Weak-hand redeal options.
- Misdeal handling options.
- All-pass handling options, including распасы reverse-scoring.
- Add talon toggles.
- Talon size (0 / 2 / 3).
- Talon distribution options.
- Talon-flip-after-first-round.
- Pass-the-talon.
- Buyback.
- Hidden talon on minimum-100 contract.
- Bad-talon redeal.
- Talon re-buy.
- Open discard.
- Add bidding toggles.
- Minimum opening bid.
- Maximum first-round bid.
- Increment below 200.
- Increment from 200 onward.
- Forced opening at 100.
- Forced dealer bid (бовт), distinct from the zero-tricks penalty.
- Blind bid.
- Re-entry after pass.
- Contra and redouble.
- Forced-bid concession.
- No contract without marriage.
- Negative-score bidding restriction.
- Named contract bids.
- Add marriage toggles.
- Hearts / Diamonds / Clubs / Spades values.
- Half-marriage capture bonus.
- Trump activation timing.
- Marriage announcement timing.
- Drowned-marriage rule.
- Ace marriage / тузовый марьяж.
- One trump per deal.
- Add trick-play toggles.
- Must follow suit as a guarded constant.
- Must overtake strictness.
- Must trump strictness.
- Defender must overtrump declarer.
- Lazy revoke.
- Partial trumping.
- Last-trick bonus.
- Slam bonus.
- Slam-against penalty.
- Lead-trump-after-marriage requirement.
- Add scoring toggles.
- Rounding granularity.
- Score declarer's actual deal points instead of bid on success.
- Defender contributions.
- Failed-contract distribution.
- Declarer rounding before contract check.
- Add opening-game, barrel, and endgame toggles.
- Golden deal settings.
- Barrel threshold, pit lock-in, deal count, fall-off penalty, collision rule, overshoot penalty, and reverse barrel.
- Target score, going-over-target rule, tiebreaker, and dump truck / самосвал settings.
- Add special-contract and penalty toggles.
- Mizère / no-tricks.
- Slam contract.
- Open-hand contract.
- Revoke penalty.
- Talon-look penalty.
- Showing-hand penalty.
- Zero-tricks "Бoлт / Палка" penalty.
- Cross / крест.
3.3 Built-in default templates
Each built-in is a constant RuleConfig value. No new code per
variant — only data.
- Add built-in 3-player regional templates.
Russian Thousandis canonical and default at first launch.Polish Tysiącuses 2-card talon, 10-step increments only, and strict przebijanie.Ukrainian Тисячаincludes the bolt rule and optional 2-deal barrel.
- Add built-in 2-player and 4-player templates.
Two-player Auses closed talon, 9-card hands, and draw stock.Two-player Buses fixed deal, 7-card hands, and no draw.Four-player Ahas dealer play, no talon, and 6 cards each.Four-player Bhas dealer sit out and otherwise standard 3-player rules.
- Add scripted engine tests for every built-in template.
- Built-in templates use only implemented selectable toggles.
- Each template can complete a full scripted deal.
- Template-specific edge rules are asserted.
3.4 Custom templates
- Implement custom template lifecycle and persistence.
- Clone a built-in template into an editable copy.
- Edit any toggle with live validation feedback.
- Save, rename, delete, duplicate, star, and sort custom templates.
- Persist templates via
love.filesystemas JSON. - Reject invalid templates on load with a clear error and fall back to the canonical default.
- Reject templates that enable deferred toggles.
- Reset an overridden built-in template to default.
- Import or export a template as shareable JSON.
3.5 Template UI
- Build the template picker and editor.
- Game start shows built-in templates first, then user templates.
- Editor groups toggles by the sections in this phase.
- Each toggle has short inline help linked to the relevant rules page.
- Inline validation greys out and explains incompatible combinations.
- Deferred toggles are disabled or hidden with a clear note, not silently selectable.
- "Use this template" applies it to a fresh game.
- Changing templates mid-game requires a confirmation prompt because it abandons the current deal.
- Custom templates show a diff against their parent built-in template.
3.6 Toggle gameplay
Each toggle catalogued in 3.2 lands here as engine behaviour matching
House Rules, with table-scene UI that
exposes every new affordance and reflects every flow change. A task
flips its fields from deferred to selectable (or implemented
where the engine reads the value directly), adds any sibling fields
the variants reference, wires every variant into the table scene, and
ships scripted engine tests covering every value.
- Implement players and seating gameplay.
count2 runs end-to-end under bothtwo_player_configvalues.count4 runs end-to-end under bothfour_player_configvalues.partnership_modefixed_across_tablepools partner scores and routes lead/legality through the partnership.- Table layout reflows for 2- and 4-player counts (seat positions, hand sizes, scoreboard rows).
partnership_modeshows a partner indicator on each seat and a pooled-score row on the scoreboard.four_player_configdealer_sits_outmarks the dealer's seat as inactive during the deal.count,partnership_mode,four_player_config, andtwo_player_configall flip to selectable.- Engine tests run scripted full deals for every player-count and seating-config combination.
- Implement dealing and redeal triggers.
four_nine_redealoptionaloffers the entitled player a redeal;mandatoryforces it.three_nine_redealoptionaloffers the entitled player a redeal.four_jack_redealonoffers the entitled player a redeal.weak_hand_redealstrict,loose, andcountedeach detect weakness by their documented criteria;countedreads a new sibling threshold field.misdeal_handlingsoft_penaltyrotates dealer;flat_penaltydeducts a new sibling penalty amount.all_pass_handlingpass_outrotates the deal without scoring;raspassyplays the deal under reverse-scoring.- Optional redeals (4-nine, 3-nine, 4-jack, weak-hand) prompt the
entitled player with a "Redeal?" dialog;
mandatoryshows a non-dismissible banner before redealing. misdeal_handlingshows the dealer-rotate banner undersoft_penaltyand the deduction line underflat_penalty.all_pass_handlingdistinguishes redeal, pass-out, and raspassy reverse-scoring play with their own banners.- All six fields flip to selectable.
- Engine tests cover every variant value against scripted dealing fixtures.
- Implement talon variants.
flip_after_first_roundonkeeps the talon closed during the first round of bidding.pass_the_talononlets the declarer concede the deal at the bid after seeing the talon.buybackonlets the declarer discard the hand for a fresh deal at the new siblingbuyback_penalty.hidden_on_minimum_100minimum_100_onlyandany_forced_100suppress the talon reveal to defenders.bad_talon_redealany_contractandminimum_100_onlyredeal on a worthless talon (threshold lives in the new siblingbad_talon_threshold).open_discardondeals the declarer's discards face-up.pass_the_talonadds a "Concede deal" button after the talon reveal;buybackadds a "Buy back hand" button with the active penalty.hidden_on_minimum_100keeps the talon closed to defenders;bad_talon_redealshows a "Redeal — bad talon" modal driven by the same pattern as the dealing-time redeal modal.flip_after_first_roundshows the talon closed during the first round of bidding and flips it once a second round opens.open_discarddeals the declarer's discards face-up at the table.- Six house-rule fields flip to selectable
(
flip_after_first_round,pass_the_talon,buyback,hidden_on_minimum_100,bad_talon_redeal,open_discard); three new sibling fields (buyback_penalty,bad_talon_threshold,rebuy_contract_value) land in the schema.sizeanddistributionkeep their current status;rebuystays deferred. - Engine tests cover every variant value against scripted talon scenarios.
- Implement talon rebuy.
rebuyontriggers a second auction at therebuy_contract_valueafter talon reveal; the rebuyer becomes the new declarer.- Table scene adds a "Buy talon at higher contract" affordance after the reveal.
rebuyflips to selectable; engine tests cover scripted rebuy scenarios end-to-end.
- Implement Polish 2-card talon distribution.
distributionpass_without_takingruns the Polish flow where the declarer never picks the talon up.size = 2lifts the engine'sunsupported_talon_sizeguard for the Polish layout.- Talon area shrinks to two cards; pickup phase is replaced with a direct pass affordance.
distributionflips to selectable forpass_without_taking;sizeflips to implemented; engine tests cover a scripted full Polish deal.
- Implement 2-player stock_draw talon distribution.
distributionstock_drawruns the 2-player Schnapsen-style per-trick draw on top of the existing 2-player Variant A stock infrastructure.distributionflips to selectable forstock_draw; engine tests cover a scripted full 2-player Variant A deal.
- Implement bidding house rules.
forced_openingonforces forehand to open at the minimum bid.forced_dealer_bidonlands the dealer in the minimum-100 contract when everybody else passes.blind_bidfirst_bid_doubledoubles a successful or failed in-the-dark first bid.re_entry_after_passonlets a passed player re-enter the auction once.contracontra_onlyarms defender doubles;contra_and_redoubleadds the declarer's redouble; the engine doubles or redoubles the contract value accordingly.forced_bid_concessionequal_split,each_full, andpreset_ratioeach split the conceded bid by their documented rule.no_contract_without_marriageno_120_without_marriageblocks bids ≥ 120 without a held marriage;capped_by_marriagescaps the maximum bid at120 + held marriage values.negative_score_restrictiononlimits a player at negative running score to the forced minimum-100 contract.named_contractsonadmits the special-contract bids at the auction.forced_openinggreys out forehand's pass button on the first bid.forced_dealer_bidshows a banner assigning the dealer the minimum-100 contract when everyone passes.blind_bidadds a "Bid blind" button before hand reveal and shows the doubling on outcome.re_entry_after_passexposes a "Re-enter" affordance to passed players.contraadds defender "Contra" and declarer "Redouble" buttons before play.forced_bid_concessionadds a "Concede" button with the active split mode previewed.no_contract_without_marriagegreys out bids ≥ 120 (or above the marriage cap) when the player holds no marriage.negative_score_restrictionlocks the restricted player's bid panel to "Take 100".named_contractssurfaces the special-contract bid buttons in the auction.- All nine fields flip to selectable.
- Auction state-machine tests cover every variant value.
- Implement marriage house rules.
half_marriage_capture_bonusoncredits the defender who captures both K and Q of the same suit; the bonus value lives in a new sibling field.trump_activation_timingimmediatere-ranks cards already played to the declaring trick.marriage_announcement_timinghand_announcementlets the leader declare without leading the K or Q;pre_first_trickrestricts declarations to the moment before the first trick.drowned_marriageretroactive_cancelcancels declared marriages when a half is later captured.ace_marriageonscores the four-aces bonus;sets_trumpsets trump from the first Ace led after declaration.one_trump_per_dealonkeeps only the first declared marriage as the trump trigger.half_marriage_capture_bonusshows the bonus row in the deal scoreboard when defenders capture both halves.trump_activation_timingimmediateplays a re-rank animation for cards already in the declaring trick.marriage_announcement_timingexposes "Announce marriage" affordances at the points the active variant allows.drowned_marriageretroactive_cancelplays a cancellation banner when the half is captured.ace_marriageadds a "Declare four aces" affordance and an ace-marriage scoreboard row.one_trump_per_dealsuppresses the trump-flip animation on later marriages while still showing the bonus.- All six fields flip to selectable.
- Engine tests cover every variant value against scripted marriage scenarios.
- Implement trick-play house rules.
must_overtake_strictnesspolish_strictenforces the Polishprzebijanieescalation when following suit.must_trump_strictnesspolish_strictextends the same escalation to trump and overtrump obligations.defender_must_overtrump_declareronforces a defender to overtrump declarer's trump when able.lazy_revokeononly punishes a misplay when called before the next lead.partial_trumpingonlets a defender holding only a lower trump discard rather than play it.last_trick_bonusoncredits the winner of the eighth trick; the bonus value lives in a new sibling field.slam_bonusfixedadds a new sibling fixed bonus on a clean sweep;doubled_biddoubles the contract value on success.slam_against_penaltyonpenalises a declarer who takes zero tricks.lead_trump_after_marriageonforces a trump lead on the trick after a marriage declaration.- Stricter legality variants (
polish_strict,defender_must_overtrump_declarer) update the legal-action highlights on each player's hand. lazy_revokeonly flags misplays during the short window before the next lead.partial_trumpinglights up the discard affordance when only lower trumps are held.last_trick_bonus,slam_bonus, andslam_against_penaltyeach add their bonus or penalty row to the deal scoreboard with a matching animation.lead_trump_after_marriagehighlights only trump cards as legal on the trick after a marriage.- All nine fields flip to selectable.
- Trick-resolution and legality tests cover every variant value.
- Implement scoring house rules.
actual_points_on_successonscoresmax(bid, actual deal points)for the declarer.defender_contributionspooledsums and splits defender deal points equally.failed_contract_distributionsplit_among_defenders,each_defender_full, andmirrors_forced_concessioneach distribute the failed bid by their documented rule.declarer_rounding_before_contract_checkonrounds the declarer's captured points before comparing them to the bid.actual_points_on_successshows the actual deal points alongside the bid when the override applies.defender_contributionspooledcollapses defender rows into a pooled-score row.failed_contract_distributionrenders the active split in the deal scoreboard.declarer_rounding_before_contract_checkshows the rounded total beside the raw captured points.- All four fields flip to selectable.
- Scoring tests cover every variant value against scripted deal outcomes.
- Implement opening-game, barrel, and endgame house rules.
golden_dealonforces every player in turn through a mandatory 120 contract for the opening N deals; the marriages-doubled, blind-allowed, and penalty-doubling sub-flags live in new sibling fields.pit_lock_inonadds an intermediate lock-in score; the pit score lives in a new sibling field.collision_rulefirst_mounterandall_collide_fall_offeach resolve simultaneous barrel arrivals by their documented rule.overshoot_penaltyonpenalises a hero-bid above 120 from the barrel.reverse_barrelonmirrors the barrel state machine at -880.going_over_targetexact_onlycaps the running total attarget_score - 1until a player lands on it exactly.tiebreakerhigh_scoreandcontinuationeach break ties by their documented rule.dump_truckpositive_onlyresets the running total at +555;both_signstriggers at ±555.golden_dealshows a "Golden deal — forced 120" banner for each opening deal, skips the bidding step, and assigns the contract directly to the next player in turn.pit_lock_inshows the pit marker on the scoreboard (e.g. "700 lock — must clear").collision_ruleresolves simultaneous barrel arrivals with a banner that names the active rule.overshoot_penalty,reverse_barrel, anddump_truckeach render their state on the scoreboard with a matching animation.going_over_targetexact_onlyshows a "must land exactly" indicator above the target row.tiebreakerresolves cross-target ties with a banner that names the active rule.- All eight fields flip to selectable.
- Scoring and state-machine tests cover every variant value.
- Implement special contracts.
mizereonadmits the zero-tricks no-trump contract; the contract value lives in a new sibling field.slam_contractonadmits the all-tricks contract; the contract value lives in a new sibling field.open_handonplays the deal face-up with doubled scoring on success and failure.- The engine's auction, trick-play, and scoring all honour each special end-to-end.
- The auction surfaces "Mizère", "Slam", and "Open hand" bid
buttons under
named_contracts. - The deal scene shows the active contract as a banner ("0 tricks goal", "all 8 tricks", "open hand") and adapts legality, scoring, and visibility accordingly.
open_handreveals the declarer's hand to all seats for the duration of the deal.- All three fields flip to selectable.
- Engine tests run scripted full deals for every special contract.
- Implement penalty house rules.
revokeflatdeducts a fixed 120;configurablededucts a new sibling fixed amount.talon_lookstricterforfeits the deal and awards the bid to the opposing side.showing_handstrictdeducts the full bid.zero_tricksconsecutive_threeandany_three(бoлт / палка) track and penalise zero-trick streaks; the declarer-exempt and golden-deal-doubled sub-flags live in new sibling fields.crossonaccumulates two crosses before deducting a fixed penalty; the penalty value lives in a new sibling field.- Each penalty (revoke, talon-look, showing-hand, zero-tricks бoлт, cross) shows a deduction animation and a deal-scoreboard row when triggered.
- The bolt and cross counters are visible on the running scoreboard so players see how close they are to the threshold.
- All five fields flip to selectable.
- Penalty engine tests cover every variant value.
- Extend the built-in template engine tests to full scripted deals.
Polish Tysiącplays a scripted deal undertalon.size = 2once the talon-variants gameplay task lifts the dealer'sunsupported_talon_sizeguard.Two-player AandTwo-player Beach play a scripted deal once the players-and-seating gameplay task lifts the dealer'sunsupported_player_countguard forcount = 2.Four-player AandFour-player Beach play a scripted deal once the same task lifts the guard forcount = 4.- Tests live alongside
tests/spec/core/builtins_spec.luaand replace the existing typed-error pins.
- Implement named-contract scoring & play.
- Follow-up to the Phase 3.6 bidding-house-rules wiring: the
auction now accepts mizère / slam / open-hand bids and the table
scene renders the buttons, but
Session:on_auction_endreturnsnot_yet_supported_named_contractwhen one wins becausecore/scoring.luadoes not yet understand structuredfinal_bidvalues. This task wires the scoring + play paths so a winning named bid becomes a playable deal. - Define mizère scoring (declarer must take zero tricks; canonical contract value 120; failure penalises the bid amount).
- Define slam scoring (declarer must take all eight tricks; contract
value reads from
bidding.slam_contract_valueonce that sibling field exists). - Define open-hand scoring (declarer plays face-up; success and failure both doubled per house-rules.md).
- Extend
core/scoring.luato dispatch ontype(final_bid) == "table"; route to the per-contract scorer. - Drop the
not_yet_supported_named_contractstub fromapp/session.lua'son_auction_end. - Engine + session + e2e tests covering the three contracts.
- Follow-up to the Phase 3.6 bidding-house-rules wiring: the
auction now accepts mizère / slam / open-hand bids and the table
scene renders the buttons, but
3.7 Canonical Russian alignment with book ruleset
Goal: align canonical_russian and the engine with the "common
standard" Russian rules described in the reference book
(TMP - Rules according to a book.md). The audit identified six
engine gaps and a small set of default-value mismatches; this
section closes the gaps (or marks them explicitly deferred), updates
the canonical defaults, and brings the docs in line with the book's
terminology.
Gaps the audit flagged against the book's standard:
- Not implemented: marriage trick-required precondition, write-off / сдача mid-deal concession, no-win-for-3-rounds penalty, every-third-write-off penalty, reset-to-zero on third barrel fall-off, dark-game stick doubling.
- Partially implemented: dedicated "two nines in the widow"
redeal trigger (today subsumed by the
bad_talon_thresholdpath), multiple-players-on-the-barrel coexistence (the existing comment incore/rule_config.luamentions a futurecoexistvalue that is not inallowed), configurable dump-truck threshold (canonical ±555 hard-coded; book mentions a +550 variant). - Out of scope (explicitly deferred): 32-card deck variant (book's optional 6-A deck with sevens = 7, eights = 0), cut-deck nine/jack penalty (procedural, not suitable for software simulation).
The work is grouped into four agent-sized chunks. Tasks 1–3 close the engine gaps; the closing task flips the canonical defaults and brings the docs in line. The write-off task lands first because the every-third-write-off counter in task 2 depends on it.
-
Implement write-off / сдача and the every-third-write-off penalty.
- Book: a declarer who sees they cannot make their contract may
"write off" — subtract the full bid from themselves and credit
half of the bid to each opponent. Every third write-off then
triggers the standard 120 penalty. Distinct from
bidding.forced_bid_concession, which only fires on a forced 100 contract. - Add
bidding.write_off(allowed{"off", "on"}, default"off") andbidding.write_off_split(allowed{"half_to_each", "equal_split"}, default"half_to_each"). - Add
penalties.write_off_streak(allowed{"off", "any_three"}, default"off"),penalties.write_off_streak_threshold(default 3, bounds[2, 5]), andpenalties.write_off_streak_penalty_amount(default 120, bounds[0, 240]). - Engine surfaces a "Write off" action to the declarer between
tricks (before the eighth trick) and applies the configured
split, pooling credit through the active
scoring.defender_contributionson partnerships. - Engine maintains a per-seat write-off counter; on every Nth write-off the penalty fires and the counter resets. The counter survives auto-save / resume.
- All five fields flip to selectable. Engine + session tests cover canonical, partnership, and 2-/4-player layouts; counter triggering and reset are exercised across scripted multi-deal runs.
- Table scene exposes a localised "Write off" button when the toggle is on; the running scoreboard shows the write-off counter so players can see how close they are to the threshold.
- Book: a declarer who sees they cannot make their contract may
"write off" — subtract the full bid from themselves and credit
half of the bid to each opponent. Every third write-off then
triggers the standard 120 penalty. Distinct from
-
Implement the cross-deal counter penalties: no-win-for-3-rounds, three-falls reset, and dark-game stick doubling.
- Book: a seat with no win in 3 consecutive (or total) rounds takes the 120 penalty; a player who fell off the barrel three times has their running total reset to zero; in a dark (blind-bid) game the received stick is doubled.
- Add
penalties.no_win_streak(allowed{"off", "consecutive_three", "any_three"}, default"off"),penalties.no_win_streak_threshold(default 3, bounds[2, 5]), andpenalties.no_win_streak_penalty_amount(default 120, bounds[0, 240]). Shape mirrors the existingzero_trickscluster. - Add
barrel.fall_count_resets_to_zero(allowed{"off", "on"}, default"off"); the third fall overrides the standardfall_off_penaltyand zeroes the running total. - Add
penalties.zero_tricks_dark_game_doubled(allowed{"off", "on"}, default"off"); shape mirrors the existingzero_tricks_golden_deal_doubledsub-flag. - Engine tracks per-seat winless-streak and barrel-fall-off counters; both survive auto-save / resume. "Winning a deal" is pinned in the scoring spec as the declarer making contract or a defender capturing positive deal points, so the streak rule cannot drift downstream.
- Under
consecutive_three, the no-win-streak counter resets on any winning deal; underany_three, only the penalty trigger resets it. - When
bidding.blind_bidis active for the deal and the dark- game toggle is"on", a zero-tricks seat earns 2 sticks instead of 1. - All five fields flip to selectable. Scoring engine tests cover every counter path across scripted multi-deal runs; auto-save round-trips assert the counters persist.
- Running scoreboard shows the no-win and barrel-fall counters next to the existing zero-tricks/cross counters so players see their progress toward the threshold.
-
Implement marriage trick-required precondition and the minor book toggles.
- Book default: a marriage may only be declared once the seat has already taken at least one trick; the trickless variant is the exception. Plus a cluster of smaller book items needed for parity.
- Add
marriages.trick_required(allowed{"on", "off"}, default"on"); the engine refuses K-Q lead-time declaration when the seat has no captured trick.marriage_announcement_timinginteracts correctly under both values (hand_announcement/pre_first_trickstill gate on the trick requirement). The four-aces ace marriage already requires a trick — sanity-check the path under the new shared field. - Extend
barrel.collision_rulewith a new"coexist"value (multiple players sit on the barrel simultaneously, each running their own countdown). Implement fully or land it deferred with a comment citing this task. - Add
endgame.dump_truck_threshold(default 555, bounds[100, 1000]) so a table can opt for the +550 variant the book mentions; the existingdump_trucktoggle keys off this value. - Add
dealing.two_nines_in_talon_redeal(allowed{"off", "any_contract", "minimum_100_only"}, default"off") as a dedicated trigger distinct from the threshold-based bad-talon redeal. - Mark 32-card deck (book's optional 6-A deck with sevens = 7, eights = 0) and the cut-deck nine/jack penalty as explicitly deferred in the schema with comments naming the deferral reason ("out of scope for v1" / "procedural; not suitable for software simulation").
- All four newly-selectable fields flip to selectable; engine tests cover marriage trick-required across forehand-leads-first scenarios and the four-aces path. Schema specs cover the new sibling fields and the deferred markers.
- Built-in templates that legitimately allow trickless
declaration pin
marriages.trick_required = "off"explicitly.
-
Update
canonical_russiandefaults to match the book's standard and refresh the documentation.- Pin
dealing.misdeal_handlingat"soft_penalty"(book: "if a penalty is received during dealing, the redeal is done by the next player"). - Pin
bidding.write_offat"on"(book describes the mid-deal concession as part of the standard action set) withwrite_off_split = "half_to_each"(book default). - Pin
penalties.write_off_streakat"any_three"(penalty system entry #5: "at every third write-off"). - Pin
penalties.no_win_streakat"any_three"(penalty system entry #4: "no win for 3 rounds in a row or in total"). - Pin
barrel.fall_count_resets_to_zeroat"on"(book: "Reset to zero. Occurs … if a player sat on the barrel 3 times and then fell off it"). - Pin
penalties.zero_tricksat"any_three"(penalty system entry #3: every-three-sticks penalty across the game). - Pin
endgame.dump_truckat"both_signs"withdump_truck_threshold = 555(book: ±555 reset to zero). - Pin
marriages.trick_requiredat"on"(book: marriage may only be declared once the seat has taken a trick). - Pin
talon.bad_talon_thresholdat4(book: "sum in the widow is less than 4"); inert untilbad_talon_redealis flipped on. - Leave
penalties.zero_tricks_dark_game_doubled, the four redeal triggers, ace marriage, golden deal, and coexist barrel collision at their"off"defaults — the book either marks them as agreed-in-advance or uses "may be" wording that frames them as variants. - Non-Russian built-ins (
polish,ukrainian,two_player_a,two_player_b,four_player_a,four_player_b) override any new default that would change their scripted scoring or legality. - Update every spec that asserts the old defaults; the full
tests/spec/core/builtins_spec.luascripted deals still pass. docs/rules/*adopts the book's vocabulary where it fits naturally: widow / прикуп, stick / pole, boast / overboast, dump truck / самосвал, write-off / сдача.docs/variations/russian.mdlists the canonical defaults explicitly (the eight book-standard rules above) and the toggles that remain off-by-default.docs/variations/house-rules.mdgains entries for the new toggles introduced in tasks 1–3 (write-off, every-third- write-off, no-win-streak, three-falls reset, dark-game stick double, coexist barrel collision, dump-truck threshold, two-nines-in-talon redeal).docs/variations/index.mdadds a "Comparison with the reference book" section that lists every gap that stays deferred after 3.7 (32-card deck, cut-deck procedural rule).
- Pin
3.8 Optional procedural deck-cut ritual
Goal: turn the cut-deck nine/jack rule into an opt-in interactive
pre-deal ritual. After the shuffle the participant counter-clockwise
of the dealer cuts the deck; a 9 or J at the bottom is a bad cut,
the next seat counter-clockwise takes the next attempt, and on the
third bad cut the dealer takes the standard 120-point penalty.
Distinct from the canonical Russian default cut_deck_safety = "on", which deterministically swaps a safe partner into the bottom
slot at shuffle time so the offence never arises — the procedural
ritual is the alternative for tables that want the cut to be a real
moment of play.
- Implement the cut-deck ritual and rotating cutter penalty.
- Book: the participant counter-clockwise of the dealer cuts the deck; a 9 or J at the bottom is a bad cut and the deck must be cut again; the third bad cut penalises the dealer.
- Flip
dealing.cut_deck_nine_jack_penaltyfrom deferred to selectable: allowed{"off", "on"}, default"off". The rule is an agreed-in-advance variant; the canonical Russian template stays oncut_deck_safety = "on"and leaves this off. - Cross-field invariant:
cut_deck_nine_jack_penalty = "on"is incompatible withcut_deck_safety = "on". The shuffle-time guard erases the offence the ritual relies on, so the combination is rejected at validation rather than producing silent dead code. - Engine: add a pre-auction
cutphase. The active cutter starts at the seat counter-clockwise of the dealer; on a bad cut the cutter rotates one seat counter-clockwise and a per-deal bad-cut counter increments. On the third bad cut the dealer takes a fixed −120 penalty (book default; configurable amount deferred to a follow-up if a table asks for it) and the deal proceeds with the current ordering — no further re-cuts. Counter resets per deal. - Session: expose
Session:cut_deck()as the active cutter's action andSession:active_cutter()/Session:bad_cut_count()for the UI and bot. Cut-phase state survives auto-save / resume. - Bot: bots auto-cut as soon as the cut phase opens for their seat; the action carries no decision so latency matches the existing minimum "thinking" delay.
- UI (table scene): when the cut phase is active, show the deck centered with a localised "Cut the deck" button on the active cutter's side. Hover / focus / active states and keyboard activation per project standard; hit target sized for touch. Bad-cut counter visible to all seats; the threshold penalty fires through the existing penalty-notification path. The ritual lives inline before the deal animation — no new modal.
- Tests: cover the happy path (good cut → straight to dealing),
rotation on bad cut, threshold penalty firing on the third bad
cut, per-deal counter reset, the invariant rejecting
cut_deck_safety = "on"+cut_deck_nine_jack_penalty = "on", the bot auto-cut path, and an auto-save round-trip with the cut phase mid-flight. - Docs:
docs/rules/dealing.mddescribes the optional ritual;docs/variations/house-rules.md"Bottom-card guard (Cut-deck safety)" cross-links the new toggle as the procedural alternative;docs/variations/index.md"Comparison with the reference book" drops cut-deck nine/jack from the deferred-rules list (only the 32-card deck remains).
3.9 Write-off as a between-talon-and-tricks prompt
Goal: align the write-off action with the book's pre-play framing. The book describes write-off as a one-shot decision the declarer makes after seeing the widow ("if it is not possible to score the ordered amount"). Today the engine surfaces the Write off button between tricks — too lenient — so a declarer can keep playing, realise the contract is dead, and concede mid-deal.
-
Move write-off out of the tricks panel into a dedicated pre-tricks decision phase.
- Engine: introduce an
awaiting_write_off_decisionphase that opens after the talon discard / Polish pass, before the first trick is dealt. Declarer chooses Play or Write off; Play transitions to tricks normally. Write off fires the existing half-bid distribution and the every-third-write-off counter. - Position the prompt between talon take and the pass step,
not between talon discard and the first trick. The declarer
has full hand information from the moment they pick up the
talon; forcing the pass (and any 2-player B discard) before
the write-off is wasted ceremony when the declarer is going
to concede. The Polish
pass_without_takingdistribution has no take step — there the prompt fires after the talon reveal and before the two opponent passes. - Session API:
Session:write_off()already exists; gate it on the new phase only. AddSession:accept_play()(or rename) for the Play branch. - Bot: bots auto-decide based on hand strength; for Phase 3.9 expose the API and ship a placeholder "always play" stub — Phase 4.5 task already covers bidding-house-rules bot logic and can extend this with a real heuristic.
- UI (table scene): swap the inline tricks-phase write-off button for a Play / Write off modal that surfaces only during the new phase. The modal lives between the talon discard animation and the first trick lead.
- Tests: cover the happy path (Play → tricks proceed), the write- off path (declarer pays the bid, opponents share half), the every-third-write-off counter still fires correctly, the new phase round-trips through auto-save, and the legacy "write off mid-tricks" path is rejected.
- Docs:
docs/rules/scoring.mdanddocs/variations/house-rules.mdnote that write-off is now a pre-tricks decision rather than an in-trick interrupt.
- Engine: introduce an
-
Replace the Play / Write off modal with an inline Write-off affordance during the pass step; clicking the inline button opens a destructive-action confirmation modal.
- Engine: drop the blocking
write_off_decision_guardfrompass_talon/pass_polish_talon/discard_talon/raise/skip_raise; have those mutators implicitly resolve the offer (equivalent to a silentaccept_play()) before applying. The explicitSession:accept_play()andSession:write_off()APIs stay for callers that want them. - UI (table scene): stop auto-opening the Play / Write off modal.
Render an inline Write off button alongside the pass
affordances whenever
view.write_off_promptis set; the button vanishes the moment the offer clears (i.e. on the first card passed). Clicking the button opens the existing modal, now framed as a destructive-action confirmation ("Cancel" / "Write off"). - i18n: add
scene.table.write_off_inline.label; relabelscene.table.write_off_prompt.declineto read "Cancel" in every locale. - Tests: unit coverage that pass / discard / raise auto-clear the
offer; the journey spec asserts no auto-modal at the prompt
phase, the inline button replaces it, "Cancel" leaves the offer
open, and confirming triggers
Session:write_off. - Docs:
docs/rules/scoring.mdanddocs/variations/house-rules.mddescribe the inline affordance.
- Engine: drop the blocking
3.10 Template editor: "current value" visual for read-only built-ins
Goal: when editing a built-in (read-only) template, the selected option in each toggle group must be visually distinguishable from the other disabled options. Today every option renders in the same greyed-out state, so the player can't tell which value is currently set without consulting the docs.
- Add a visual marker for the selected-but-disabled state in
the template editor.
- UI (template editor): introduce a
currentindicator (outline ring, "Current" pill, or analogous treatment that survives keyboard focus + touch) on the option whose value matches the template's current setting. Applies to enum-leaf groups, boolean toggles, and number steppers alike. - Read-only behaviour stays unchanged: the option is still non-interactive in built-ins; the marker is purely informational.
- Editable templates (clones) keep the existing selected-active visual and don't need the marker — the active state already communicates the same thing through ordinary affordance.
- Tests: a UI scene spec asserts the marker is rendered for the canonical Russian template and absent for an editable clone.
- Docs: no doc change — the template-editor screenshot in
docs/development/architecture.mdis a future Phase 5 deliverable.
- UI (template editor): introduce a
3.11 Decide canonical Russian all-pass behaviour
Goal: pick a book-faithful default for the all-pass case. The book itself is silent on what happens when every seat passes the auction, but two video walkthroughs of the canonical Russian rules both describe the dealer being implicitly committed to a 100 contract before the auction starts ("the dealer has 100 hanging by default" / "the dealer automatically bids 100. After two passes, the remaining player gets the contract"), which rules out the all-pass redeal scenario.
- Choose between the two book-faithful readings and pin the
defaults accordingly.
- Reading B chosen.
bidding.forced_dealer_bidflipped to"on"incanonical_russian; the auction can no longer collapse on all-pass. - Non-Russian built-ins explicitly opt back off so the Russian
flip doesn't leak: Polish, 2-player A/B, 4-player A get
forced_dealer_bid = "off". Ukrainian inherits "on" (matches the bolt rule the book mentions obliquely). 4-player Configuration B keeps "off" because the dealer sits out — theforced_dealer_bid_requires_active_dealerinvariant requires it. docs/variations/russian.mdlists forced-dealer-100 in the canonical defaults section;docs/variations/index.md"Comparison with the reference book" calls it out;docs/variations/house-rules.md"Forced dealer bid" notes the canonical default.- Tests that exercised the old all-pass terminator
(
auction_spec,talon_spec,session_spec,session_redeal_spec,table_view_model_spec) opt back off explicitly so the path stays reachable.
- Reading B chosen.
Phase 4 — Bot opponents (algorithmic)
Goal: a single human plays against the rest of the seats filled by
algorithmic bots. No LLM yet — silent bots. The hard requirement is
legal play under every built-in RuleConfig. Every selection a human
makes at the table — every modal, every toggle-gated affordance, and
every per-turn action introduced by Phase 3.6 — has a matching bot
hook so a non-human seat can be driven without clicks. Strategy
quality starts with the canonical Russian template and improves
incrementally; legality is universal from the first commit.
The bot module observes a read-only session view and returns engine
actions. It must never mutate state directly, never import from
ui/, and never import from app/llm/. CI guards the import graph.
Phase 7 (LLM characters) extends presentation only — moves stay
algorithmic.
4.1 Bot interface and engine-driver loop
- Define the bot player interface in
app/bot/.- Interface covers
chooseBid,chooseTalonPass,chooseRaise,chooseCard, andchooseMarriage, plus an entry for every additional decision surface introduced by Phase 3.6: accept or decline a redeal offer, accept or decline a bad-talon redeal, claim or decline a rebuy, take the talon vs concede vs buy back, pass talon cards (Russian, Polish, 2-player-B discard), raise or skip raise, and start the next deal at deal-done. - Each entry is a pure function over a read-only session view and the active seat; it returns the action to invoke on the engine, not the mutation.
- The view exposes only the existing read-only session accessors
(
hands,legal_cards,current_turn,current_phase,current_bid,current_trick,trump,talon_cards,redeal_offer,bad_talon_offer_state,rebuy_offer_state,available_marriages,config); no UI or LLM imports. - CI guards that
app/bot/imports nothing fromui/orapp/llm/.
- Interface covers
- Wire the bot driver into the table scene.
- When
current_turn()is a bot seat, the table scene calls the bot module for that seat and applies the returned action via the same Session mutator a human button would call. - Modal awaiting-state phases (
awaiting_redeal_decision,awaiting_bad_talon_decision,awaiting_rebuy_decision) use the same path: the offer state names the responsible seat, the bot decides, and the scene routes accept or decline. - One-shot intra-phase affordances (talon take / concede / buy back, pass talon per opponent, raise or skip raise, Polish pass, 2-player-B discard, declare-marriage-then-play) chain through the same loop — the bot returns one action per call until the phase moves on.
- A "thinking…" indicator runs while a seat is deciding; effective decision latency is capped at 2 s including the indicator.
- When
- Route in-trick marriage declarations through the driver.
- The 4.1 commit ships
tricks → choose_card;choose_marriageis in the registry but not yet routed. The Phase 4.3 marriage heuristic decides whether to declare on lead, then chainschoose_marriage→ apply →choose_cardon the next tick.
- The 4.1 commit ships
- Add a chooser for the cut-deck phase.
Session:current_phase() == "cut"has no chooser entry today; bot seats stall there. Addchoose_cut_deckto the contract registry and a stub returning the canonical deck split, so a bot-driven game undercut_deck_safety = "on"does not block.
4.2 Single-player mode and seat assignment
- Add single-player mode and per-seat bot/human assignment.
- Single-player is selectable from the main menu and starts a
game with one human seat and the rest filled by bots under the
active
RuleConfig. - The new-game flow also exposes per-seat bot vs human pickers so mixed compositions (e.g. 2 humans + 1 bot) work for any supported player count.
- The hot-seat privacy curtain is suppressed for bot seats — no pass-the-device prompt before a bot move — but stays between consecutive human seats under the existing privacy toggle.
- Save format records each seat's bot binding alongside the
RuleConfigsnapshot so saved games restore with the same composition. (Phase 5.3's "assigned characters per seat" requirement extends this binding; Phase 7 fills the character layer.)
- Single-player is selectable from the main menu and starts a
game with one human seat and the rest filled by bots under the
active
- Lock the human seat's perspective in single-player and mixed
compositions.
- The view-model treats the lone (or first) human seat as the
viewer instead of following
current_turn(), so the human's own hand stays in the bottom "self" slot and bot seats render as face-down opponent strips for the entire deal. - Mixed compositions with multiple human seats keep the existing
hot-seat behaviour: the viewer follows
current_turn()between human seats and the privacy curtain hands off as today. - The action panel (auction, talon, marriage, trick affordances) is hidden while a bot is on turn; only the existing "Bot N thinking…" banner remains, so the human can't accidentally interact with a bot's decision surface.
- Talon visibility still honours
talon.hidden_on_minimum_100from the human viewer's seat, not from whichever bot is on turn.
- The view-model treats the lone (or first) human seat as the
viewer instead of following
- Add bot difficulty levels.
- Easy, normal, and hard differ in bidding aggression, marriage planning, trump leading, discard quality, and — where the rule set permits it — defender cooperation.
- Difficulty is a per-seat setting on the new-game flow.
4.3 Baseline-legal bot play for shipped decision surfaces
These tasks land legal-only bot logic for every surface that already exists after Phase 2 plus Phase 3.6's first half. Strategy tuning happens in 4.4.
- Implement legal bot play for the auction.
- The bot bids or passes within
bidding.opening_min,bidding.pre_talon_max, and the active increment thresholds. - The bot never proposes an illegal bid and never passes when the engine forbids it.
- The bot bids or passes within
- Implement legal bot play for redeal offers.
- The bot accepts or declines
four_nine_redeal,three_nine_redeal,four_jack_redeal, andweak_hand_redealmodal prompts duringawaiting_redeal_decision. - Mandatory variants are auto-accepted; optional variants default to accept on weak hands and decline on borderline ones, configurable by difficulty.
- The bot accepts or declines
- Implement legal bot play for the talon flow.
- At
talonrevealed status the bot picks one of: take talon, concede deal (pass_the_talon), buy back hand (buyback), or — underpass_without_taking— pass two talon cards directly to opponents, against the activetalon.distribution. - After taking, the bot passes one card to each opponent; under
closed_talon_draw_stockit follows the 2-player Variant A flow and underfixed_deal_no_drawit picks the discard card. - The bot raises by one valid increment when the talon clearly
boosts the bid value, otherwise calls
skip_raise.
- At
- Implement legal bot play for bad-talon and rebuy modals.
- At
awaiting_bad_talon_decisionthe bot accepts a redeal when the talon points are below the active threshold and the contract is in the gated range; otherwise it declines. - At
awaiting_rebuy_decisioneach defending bot claims rebuy attalon.rebuy_contract_valueonly when the held hand justifies the forced contract; otherwise it declines and the queue advances to the next defender.
- At
- Implement legal bot play for tricks.
- The bot obeys must-follow, must-beat, must-trump, and overtrump
rules via the engine's
legal_cards()helper. - On lead the bot considers
available_marriages()and can declare a marriage by leading the K or Q. - Under 2-player Variant A the bot continues to play through both
tricks_phase()substates (drawandstrict). - Bot move legality holds under every built-in
RuleConfig, not just Russian.
- The bot obeys must-follow, must-beat, must-trump, and overtrump
rules via the engine's
- Implement legal bot play for deal-done.
- When the seat at
deal_doneis a bot, the table scene auto- callsstart_next_deal()after a short pause that lets the final-deal scoreboard settle.
- When the seat at
4.4 Strategy tuning (canonical Russian first)
- Tune bot bidding heuristics against the canonical Russian template, following the Strategy page.
- Tune bot talon passing and raise heuristics so the declarer retains marriages and trump strength when possible.
- Tune bot trick play so the leader plans marriages, the declarer pulls trumps efficiently, and defenders cooperate where the rule set allows.
- Confirm canonical-Russian tuning against scripted full-game fixtures with known good and bad outcomes.
4.5 Bot logic for pending Phase 3.6 toggles
Each task lands the bot logic alongside the matching 3.6 toggle once that toggle's engine and UI ship. Until the matching 3.6 task lands, the toggle stays deferred for bot seats too — exactly as it is for human seats.
- Implement bot logic for bidding house rules.
forced_opening,forced_dealer_bid,blind_bid,re_entry_after_pass,contraand redouble,forced_bid_concession,no_contract_without_marriage,negative_score_restriction, andnamed_contractseach get bot behaviour matching the new affordance.
- Implement bot logic for marriage house rules.
marriage_announcement_timinghand_announcementandpre_first_trickuse the new declaration affordances.- Under
ace_marriagethe bot declares the four-aces bonus when eligible. drowned_marriage,trump_activation_timing,half_marriage_capture_bonus, andone_trump_per_dealare automatic in the engine; the bot just plays cleanly under them.
- Implement bot logic for trick-play house rules.
- Stricter
polish_strictlegality,defender_must_overtrump_declarer,partial_trumping,last_trick_bonus,slam_bonus,slam_against_penalty,lead_trump_after_marriage, andlazy_revokeeach adjust card-choice heuristics.
- Stricter
- Implement bot logic for special contracts.
- Mizère: bid only when the held hand is clearly bid-safe; play every trick to lose. Slam: bid only on hand strength sufficient for an 8-trick sweep. Open hand: keep bidding aware of the visible-hand penalty/bonus; play stays algorithmic.
- Implement bot logic for opening-game, barrel, endgame, scoring, and
penalty toggles.
- Behaviour changes are mostly bidding posture (forced 100 on
golden_deal, defensive play nearbarrel.threshold, hero- bid avoidance underovershoot_penalty, exact-landing posture undergoing_over_target = exact_only). Penalty rules are automatic in the engine; the bot just plays cleanly under them.
- Behaviour changes are mostly bidding posture (forced 100 on
- Promote the per-surface heuristics from 4.3 and 4.5 over the
deterministic-legal scaffolding so the production driver's
default chooser registry no longer relies on
app/bot/stubs.lua.- The driver wiring itself stays untouched — only the default
chooser bodies change.
app/bot/stubs.lua(or its successor) remains available as the scripted-stub fallback that 4.6 wires into journey tests.
- The driver wiring itself stays untouched — only the default
chooser bodies change.
4.6 Bot legality, latency, and tests
- Add the all-bot legality guarantee.
- Every bot move passes through the same engine that gates human moves.
- A property-style test runs N random full games for each built-in template with all-bot seats and asserts no illegal action is ever proposed.
- Add a scripted bot stub for journey tests.
- The stub is deterministic and ignores strategy; it returns the first legal action for each surface.
- Journeys that script all seats by deck-seeding move to bot- driven seats backed by the stub, so the journey harness no longer needs to know which seats are scripted.
- Re-test bot play on supported desktop platforms.
- macOS and Linux runs have no regressions.
- The "thinking…" indicator and 2 s latency cap hold under the real Love2D loop.
Phase 5 — UX & polish
Goal: it looks and feels like a card game, not a prototype.
5.1 Look & feel basics
- Add polished table presentation.
- Animations cover deal, play, capture, trump-flip on marriage, and talon reveal.
- Sound effects cover card flip, card play, trick capture, marriage chime, win, and loss.
- Scoreboard clearly shows running totals, current deal contributions, and barrel state.
- Build the settings screen.
- Settings include active rule template, active card skin, language, sound on/off, animation speed, and light/dark theme preference.
- Theme respects system preference.
- Add the first-time-player tutorial and end-of-game stats.
- Tutorial walks through bidding, talon, marriage, and barrel in one guided deal.
- End-of-game stats include deals played, marriages declared, and contracts made/failed.
- Add optional background music.
- One-track music can be toggled on or off.
5.2 Card skins
- Define the card-skin asset-pack format.
- A skin directory contains 24 face cards, a card back, and table felt.
- A manifest references every asset so adding a skin is data-only.
- Ship built-in skins with readability checks.
- One clean, readable default skin ships in v1.
- At least 3 alternative built-in skins are visually distinct.
- Every skin lets a player identify any card in under 1 second.
- Build the skin selector.
- Settings show live preview before apply.
- Selected skin persists across launches and survives app updates.
- Add skin extension hooks.
- Per-skin sound overrides are supported.
- User-imported custom skins can be loaded from the save directory.
5.3 Save & load games
A complete game to 1000 can run an hour or more across many deals. Players need to set a game aside and come back to it — possibly with multiple games on the go.
- Harden auto-save checkpoints and save schema.
- Auto-save writes after each scored deal, on app suspend, and on graceful quit.
- Save format is JSON via
love.filesystem. - Save includes
schemaVersion. - Save includes active rule template identity and a full snapshot of its toggles.
- Save includes assigned characters per seat, even before Phase 7 fills character data.
- Save includes running scores, full played-deal history, in-progress deal, and created / last-saved timestamps.
- Add continue/resume from auto-save.
- Main menu shows "Continue" when an auto-save exists.
- Continue resumes mid-deal.
- Continue is disabled when no auto-save exists.
- Implement manual save slots and the saved-games list.
- Save current game between deals with a user-chosen name.
- Named slots default to 10.
- Saved-games list shows player names, running scores, deal number, and last-played timestamp.
- Implement saved-game actions.
- Load any saved game with confirmation if an in-progress game would be abandoned.
- Delete a saved game with confirmation.
- Rename a saved game.
- Sort saved games by recent or name.
- Show a table-state thumbnail or preview when available.
- Add save-file hardening and portability.
- Corrupted or schema-incompatible saves produce a clear, localised message and never crash.
- Saved games can be exported and imported as JSON.
Phase 6 — iOS port (cross-platform prototype)
Goal: the same Lua source builds and runs on macOS, Linux and iOS. The full game plays correctly on every v1 target.
- Wire the love-ios Xcode project and iOS app assets.
- Xcode project embeds the current
.love. - App icon set and launch screen are present.
- Xcode project embeds the current
- Audit shared code for iOS-safe assumptions.
love.filesystemis used exclusively.- No absolute paths are used.
- Phase 2 touch/input assumptions still hold on real iOS hardware.
- Adapt the table UI for touch and responsive layouts.
- Hit targets are at least 44 pt on iOS.
- Table layout reflows in portrait and landscape.
- iPhone and iPad are both supported.
- iPad can use extra space for scoreboard and history.
- Smoke test key UI on touch devices.
- Template picker works under touch input.
- Skin selector works under touch input.
- Save and resume work in the iOS sandbox.
- Validate the cross-platform prototype end-to-end.
- A complete game can be played on iPhone, iPad, macOS, and Linux without regressions.
- Add iOS platform polish.
- Haptic feedback fires on card play, trick capture, and marriage.
- Dynamic Type support is available for accessibility.
Phase 7 — AI characters & psychology
Goal: AI seats become named characters with personalities. With an OpenAI-compatible LLM endpoint configured, characters banter, react and attempt to bluff during play. The algorithm still picks every move — the LLM only writes text.
The algorithm picks every move. The LLM only writes dialogue. This phase must not weaken that boundary. If you find yourself wanting the LLM to choose a card "just this once", you are doing it wrong — stop and talk to the architect.
7.1 Algorithm-vs-LLM firewall
- Create the bot/LLM module boundary and import-graph guard.
app/bot/andapp/llm/live in separate modules and never import each other.- CI fails if the import graph is violated.
- The LLM client's only public output type is text.
- The LLM client has no method shape that could return a card, bid, or move.
- Prove LLM failures cannot affect gameplay.
- With the LLM client stubbed to return chaos, errors, or nothing, every test from Phases 1, 2, 3, and 4 still passes unchanged.
7.2 CharacterPreset data model
- Implement
CharacterPresetschema, game binding, and persistence.- Fields include name, avatar reference, short description, full LLM system
prompt, voice/tone hints, default algorithmic difficulty,
bluffStyle, spoken-language hint, andschemaVersion. - Each AI seat is bound to a
CharacterPresetornilfor silent AI. - Character data has no causal effect on moves.
- Presets round-trip through JSON and share schema-versioning conventions
with
RuleConfig.
- Fields include name, avatar reference, short description, full LLM system
prompt, voice/tone hints, default algorithmic difficulty,
7.3 Built-in characters
The slots matter more than the exact names; the product owner refines copy later.
- Add the built-in character cast.
- "Silent" is always available and is the default fallback when no LLM endpoint is configured.
- At least 4 named characters have distinct voices and bluff styles.
- Suggested archetypes: calculating aunt, old hustler, friendly amateur, smug newcomer.
- Each named character has an avatar asset and complete LLM system prompt.
- Built-in character UI copy is localisable.
7.4 Custom characters
- Implement custom character lifecycle and validation.
- Clone a built-in character into an editable copy.
- Edit name, description, system prompt, voice/tone, default difficulty, and bluff style.
- Save, rename, delete, and reset custom characters.
- Persist custom characters via
love.filesystemas JSON. - Reject empty names, too-long system prompts, and invalid default difficulties.
- Import or export a character as shareable JSON.
7.5 OpenAI-compatible LLM client
- Implement the non-blocking LLM client in
app/llm/.- Client calls the OpenAI Chat Completions shape, or any endpoint that mimics it.
- Base URL, API key, model name, and optional extra headers are configurable.
- Calls never stall the game loop.
- Per-call timeout defaults to 8 seconds.
- Client returns text only and never decodes move-shaped responses.
- Streaming responses can progressively update chat when supported.
- Verify endpoint compatibility.
- OpenAI at
api.openai.com. - OpenRouter.
- Local Ollama through its
/v1OpenAI-compatible mode. - LM Studio.
- OpenAI at
7.6 Endpoint settings & API key storage
- Build LLM settings and secure API-key storage.
- Settings include base URL, API key, model name, optional extra headers, and test connection.
- Defaults are blank; LLM chat is opt-in.
- Without an endpoint, AI seats stay silent and the game is fully playable.
- API key uses platform secure storage where available: macOS Keychain and iOS Keychain.
- Linux fallback is a local file with a clear in-app notice.
- API key is never logged, included in crash reports, or saved in game files.
- User can forget the API key.
- First enable shows a short "where does my data go?" explainer.
7.7 Character chat HUD
- Build the character chat UI and session controls.
- Each AI seat shows avatar and name.
- Incoming chat appears as a speech bubble or banner.
- Chat is non-modal and never blocks turns or pauses the algorithm.
- Player can mute or unmute a character from its avatar.
- Human can talk back through a per-session text input.
- Chat history is available from a sidebar or drawer.
- Character text can be auto-translated to the player's locale through the same LLM endpoint.
7.8 Cost, rate & failure controls
- Add LLM cost, rate, and silent-failure controls.
- Calls per deal are capped by a conservative user-adjustable setting.
- At most one LLM call per character is in flight at a time.
- Event sources are throttled, such as one bid reaction per character per auction round.
- Missing key, 4xx, 5xx, timeout, parse failure, and rate limit errors are silent at the table.
- Settings show the last diagnostic error for debugging.
- Per-character daily call counter is visible in settings.
- Built-in cast can use pre-baked offline lines when the LLM is unavailable.
7.9 Character editor UI
- Build the character picker and editor.
- Game start lets the player choose a character or "Silent" for each AI seat.
- Editor includes avatar slot, name, description, system-prompt textarea, voice/tone selector, default difficulty selector, and bluff style.
- "Test in chat" sends a sample prompt and shows the response without game state.
- Custom characters show a diff against their parent built-in character.
- Avatar import can load an image from the save directory.
Phase 8 — Education mode
Thousand is complicated by modern card-game standards — bidding, the talon, marriages, must-trump and the barrel are all unfamiliar concepts to many players. Education mode is the dedicated learning path for people who want to study the game rather than just play it.
Distinct from the single-deal tutorial in Phase 5, Education mode is a multi-lesson course from the main menu.
- Build the Education mode hub and lesson framework.
- Main menu has "Learn to play".
- Lesson list contains roughly seven 3–5 minute lessons.
- Lessons are interactive scripted hands with hint balloons, not pure text.
- Implement the lesson sequence.
- Lesson 1: deck and card rank.
- Lesson 2: bidding.
- Lesson 3: talon, pass, and raise.
- Lesson 4: marriages and trump flips.
- Lesson 5: trick-taking under must-follow, must-beat, and must-trump.
- Lesson 6: scoring a deal and the barrel.
- Lesson 7: variations primer for Russian, Polish, Ukrainian, 2-player, and 4-player templates.
- Add education support screens and progress.
- Glossary scene is searchable and available from a
?icon. - Progress tracking records per-lesson completion and resume state.
- Lesson copy is keyed through
t()and translated with the rest of the app.
- Glossary scene is searchable and available from a
- Add learner practice aids.
- Practice mode is a one-deal sandbox where AI offers gentle suggestions.
- Quick-reference popover is available during a real game.
- Completion badges stay limited to simple checkmarks.
Phase 9 — Release readiness
Goal: signed, localised, distributable builds for macOS, Linux and iOS.
9.1 Localisation
The i18n plumbing has existed since Phase 0; this section is translation work against keys already used in the code.
- Finalise the English source locale and populate release translations.
- English locale table is reviewed and treated as source of truth.
- Russian, Polish, and Ukrainian tables are populated against the same keys.
- Built-in character names and descriptions are localised into all four languages.
- Audit localisation in full play sessions.
- No missing-key warnings occur in any locale.
- Locale auto-detection works on first launch.
- Manual language override is available in settings.
9.2 Persistence
The save format and save/load UI already land in Phase 2 and Phase 5. This section hardens persistence for release.
- Audit auto-save across platform suspension points.
- iOS app backgrounding.
- macOS app quit.
- Linux SIGTERM.
- Terminal Ctrl-C during development.
- Add persistence migration and update survival.
- Saves from earlier versions either migrate forward or are rejected with a clear, localised message.
- User-saved rule templates, characters, and saved games survive app updates.
- Score history for the last N games is viewable in the menu.
9.3 Packaging & distribution
- Build signed release packages for v1 platforms.
- macOS
.appbundle is code-signed and notarised. - Linux
.AppImageand.tar.gzfallback are produced. - iOS
.ipais ready for App Store Connect.
- macOS
- Prepare release metadata and privacy materials.
- Store description and screenshots are ready.
- Privacy policy explicitly says LLM chat is opt-in and the user's API key never leaves the device except when calling their configured endpoint.
- Add release diagnostics and versioning.
- Crash reporting is opt-in and anonymised.
- Crash reports scrub the API key.
- Semver is stored in
conf.luaand shown in the settings About screen.
- Add desktop update guidance.
- Desktop builds include either an auto-update channel or a clear check for updates link.
Standing engineering rules
These are not checklist tasks; they apply to every phase.
- Keep Rules of Play in sync with code. If they disagree, fix the docs first.
- Every PR runs lint and tests; no merges on red.
- Every player-visible string goes through
t(). No hard-coded UI literals, including placeholders, errors, and tutorials. - No platform-conditional code in
core/orapp/. Platform-specific work lives inui/orplatform/. - No absolute filesystem paths. Use
love.filesystem. - Touch and mouse input stay equivalent so the same code covers iOS, desktop, and later Android.
- Update this task list as work changes: tick completed tasks, add newly discovered tasks in sequence, and strike obsolete tasks with a note instead of deleting them.
- Each phase-closing PR includes a short retrospective.
Phase 10 — Windows desktop (post-v1)
Goal: signed Windows build with the same Lua source.
- Arrange Windows test hardware and verify the existing
.love.- Test with stock
love.exeon Windows 10+.
- Test with stock
- Package, sign, and validate the Windows build.
- Windows
.exeor installer is produced and code-signed. - Save files land in the expected per-user location and round-trip.
- Windows
- Add Windows release polish.
- Windows-specific app icon and metadata are ready.
- CI matrix includes a Windows runner.
Phase 11 — Android (post-v1)
Goal: Play Store build with the same Lua source.
- Arrange Android test hardware and wire love-android packaging.
- Phone and tablet test devices are available.
- Gradle project embeds the current
.love.
- Re-validate Android runtime behavior.
- Touch hit-targets meet the 48 dp Android convention.
- Layout reflows across phone and tablet form factors.
love.filesystemsave round-trip works in the Android sandbox.
- Prepare the Play Store build.
- App icon, adaptive icon, and Play Store metadata are ready.
- Signed
.aabis ready for Play Console upload.
- Add Android platform polish.
- Android haptic feedback matches iOS behavior where practical.
- CI matrix includes an Android build runner.