Triples: Card Table Solver
HardAI-assisted coding · AI-Assisted Feature Work
Triples: Card Table Solver
You've joined the games team that owns Triples, a solitaire game: ten cards lie face up on a table, and on each turn you take any three that add up to 20. The daily-challenge service deals every table from a seed, and design wants tools to rate seeds. This sprint you'll fix a rules bug, build the auto-player design uses, add rule variants, and then build an exact "best possible score" search.
Codebase
triples/cards.py:Card, the ranks and suits, and the factory-order deck.triples/shuffle.py: deterministic seeded shuffle. Known-good, do not change it.triples/config.py:Variant(table size, cards per move, target total, court-card values) and the registry of variants.triples/table.py:Table: face-up slots, the stock, the move history, refill and undo. Known-good.triples/rules.py: move validation (check_move) and move enumeration (valid_moves).triples/strategy.py: auto-play strategies andplay_out(not implemented yet).triples/search.py: best-possible-score search (not implemented yet).triples/session.py:Game(a variant plus a table) andSession(games by id).triples/api.py: therun(operations)entrypoint and the operation dispatcher.tests/: existing unit tests.test_duplicate_positions_are_rejectedandtest_negative_position_is_out_of_rangeintests/test_rules.pyfail today.
How your code is run
run(operations: list[list]) -> list creates a fresh Session, applies each operation in order and returns exactly one output per operation. Errors are returned as strings, never raised.
| Operation | Arguments | Returns | Errors |
|---|---|---|---|
new | game_id, seed (int), optional variant (default "classic") | the dealt table (list of card codes) | "error: unknown variant '<name>'" |
table | game_id | the table now: a list of card codes, null for an empty slot | "error: unknown game" |
moves | game_id | every legal move, each a sorted list of slot numbers, in ascending lexicographic order | "error: unknown game" |
play | game_id, positions (list of ints) | "ok" | move errors (Part 1), "error: unknown game" |
status | game_id | {"cleared": int, "stock": int, "over": bool} | "error: unknown game" |
undo | game_id | "ok" | "error: nothing to undo", "error: unknown game" |
history | game_id | the moves played so far, oldest first, each a sorted slot list | "error: unknown game" |
autoplay | game_id | added in Part 2 | "error: not supported" today |
best | game_id | added in Part 4 | "error: unknown operation 'best'" today |
Any other operation name returns "error: unknown operation '<name>'". new on an id that already exists replaces that game.
The game. The deck has 52 cards. A card code is its rank followed by its suit, for example 10H, QS, AD. An ace is worth 1, a number card its number, and J, Q and K are worth 10 in the classic variant. The shuffled deck is dealt: the first 10 cards go face up in slots 0 to 9 and the other 42 form the stock (the rest of the deck, top card first).
A move names three different non-empty slots whose cards add up to 20. Those cards are removed and counted in cleared. Then the emptied slots are refilled with cards from the top of the stock, in increasing slot order, whatever order the move listed them in. If the stock runs out, the remaining emptied slots stay empty (null). The game is over when no legal move exists.
Worked example (seed 1 deals 9D AH 4H 9H 3C 7H 2S 2D AC 7C)
[["new", "g", 1], ["play", "g", [5, 0, 2]], ["table", "g"], ["status", "g"], ["history", "g"]]returns
[["9D","AH","4H","9H","3C","7H","2S","2D","AC","7C"], "ok",
["10S","AH","5H","9H","3C","8H","2S","2D","AC","7C"],
{"cleared": 3, "stock": 39, "over": false}, [[0, 2, 5]]]Slots 0, 2 and 5 hold 9 + 4 + 7 = 20. They are refilled with the top three stock cards, 10S, 5H, 8H, in that slot order.
Part 1: Move validation (bug report)
Goal: play must reject malformed moves.
A bug report from QA: "I can win cards twice by naming the same slot twice, and -1 seems to work as a slot." Moves are checked in this order, and the first failure is the error:
- the list does not contain exactly 3 positions:
"error: pick exactly 3 cards" - any position is below 0 or at least the number of slots:
"error: position out of range" - any position appears more than once:
"error: positions must be distinct" - any named slot is empty:
"error: empty slot" - the values do not add up to 20:
"error: cards sum to <total>, need 20"
A rejected move changes nothing (table, stock, cleared and history all stay as they were).
Example. On seed 1, ["play", "g", [0, 0, 6]] returns "error: positions must be distinct" even though 9 + 9 + 2 = 20, and ["play", "g", [-1, 0, 1]] returns "error: position out of range".
Part 2: The auto-player
Goal: implement autoplay with design's reference player, "high cards first".
["autoplay", game_id]plays the game to the end, starting from its current position, and returns{"moves": [...], "cleared": <int>}.moveslists the moves made by this call, in order, each as a sorted slot list;clearedis the game's totalclearedafterwards, including cards cleared before the call.- Each turn it chooses from the legal moves like this. Sort each move's card values from high to low (an ace is 1). Choose the move whose value list is largest when compared element by element (a move with
[9, 9, 2]beats one with[9, 7, 4]). If several moves have identical value lists, choose the one with the lexicographically smallest slot list. - It stops when no legal move remains. On a game that is already over it returns
{"moves": [], "cleared": <cleared>}. - Moves are played through the normal rules, so
status,historyandundosee them like any other move.
Example. On seed 1 the first turn has six legal moves; [0, 3, 6] (9, 9, 2) wins under rule 2. The whole game:
[["new", "g", 1], ["autoplay", "g"], ["status", "g"]]returns
[["9D","AH","4H","9H","3C","7H","2S","2D","AC","7C"],
{"moves": [[0,3,6],[0,6,7],[0,4,5],[0,3,5]], "cleared": 12},
{"cleared": 12, "stock": 30, "over": true}]Part 3: Variants
Goal: new accepts a variant name, and every rule honours the variant of the game.
| Variant | Slots | Cards per move | Target | J / Q / K |
|---|---|---|---|---|
classic | 10 | 3 | 20 | 10 / 10 / 10 |
pairs | 9 | 2 | 14 | 11 / 12 / 13 |
quads | 12 | 4 | 30 | 10 / 10 / 10 |
- Aces are 1 and number cards their number in every variant.
- Dealing, refill, the stock size and
statusfollow the variant's slot count. Each variant uses the same deck and shuffle. - Part 1's rules apply with the variant's numbers: the messages use them (
"error: pick exactly 2 cards","error: cards sum to 25, need 30"). movesandautoplayuse the variant's card values and move size. The Part 2 choice rule is unchanged.classicbehaves exactly as before, and it is the default whennewhas no variant.
Example. [["new", "p", 2, "pairs"], ["moves", "p"]] returns [["6C","2D","10H","AC","QH","2S","JD","QC","6S"], [[1,4],[1,7],[4,5],[5,7]]]: the four legal pairs are 2D + QH, 2D + QC, QH + 2S and 2S + QC (2 + 12 = 14 each).
Part 4: Best possible score (stretch)
Goal: add ["best", game_id].
- It returns the largest total
clearedthat any sequence of legal moves can reach from the game's current position (cards already cleared are included). It is the exact optimum, not an estimate. - It does not change the game:
table,status,historyandundobehave as if it was never called. - It follows the game's variant, and on an unknown id it returns
"error: unknown game". - If no move is legal, it returns the current
cleared. - Scale: tests call
beston many fresh games in one run, including the 12-slotquadsvariant, within the runner's 5-second limit for the whole suite. Trying every ordering of every move is far too slow.
Example. On seed 1 greedy gets stuck at 12, but the best line clears 48:
[["new", "g", 1], ["autoplay", "g"], ["new", "g", 1], ["best", "g"]]returns the table, {"moves": [[0,3,6],[0,6,7],[0,4,5],[0,3,5]], "cleared": 12}, the same table again, and 48.
Notes
shuffle.py,cards.pyandtable.pyare known-good. The bugs are elsewhere.- Use the AI assistant freely, but check what it changes against the rules and examples above.
- Later parts must not break earlier ones. Hidden tests re-check earlier behaviour.
- Parts 1-3 is a strong result. Part 4 is a stretch.
Example 1
- Input:
- [["new", "g", 1], ["play", "g", [0, 0, 6]], ["play", "g", [5, 0, 2]], ["table", "g"]]
- Output:
- [["9D","AH","4H","9H","3C","7H","2S","2D","AC","7C"], "error: positions must be distinct", "ok", ["10S","AH","5H","9H","3C","8H","2S","2D","AC","7C"]]
- Explanation
- Slot 0 twice plus slot 6 sums to 20, but one card cannot be taken twice. Slots 0, 2 and 5 (9 + 4 + 7) are a real move, and their slots are refilled from the stock in increasing slot order.
Example 2
- Input:
- [["new", "g", 1], ["autoplay", "g"], ["new", "g", 1], ["best", "g"]]
- Output:
- [["9D","AH","4H","9H","3C","7H","2S","2D","AC","7C"], {"moves": [[0,3,6],[0,6,7],[0,4,5],[0,3,5]], "cleared": 12}, ["9D","AH","4H","9H","3C","7H","2S","2D","AC","7C"], 48]
- Explanation
- The greedy auto-player gets stuck after 4 moves (12 cards) on seed 1, but a perfect player can clear 48.
Example 3
- Input:
- [["new", "p", 2, "pairs"], ["moves", "p"]]
- Output:
- [["6C","2D","10H","AC","QH","2S","JD","QC","6S"], [[1,4],[1,7],[4,5],[5,7]]]
- Explanation
- In the pairs variant a move takes 2 cards adding to 14 and J/Q/K are worth 11/12/13: 2D + QH = 14, 2D + QC = 14, QH + 2S = 14, 2S + QC = 14.
Guidelines
- Python 3 standard library only
- Slots are numbered from 0; a move is a list of slot numbers
- The deal is deterministic for a seed; do not touch triples/shuffle.py
- Aces count 1 and number cards their number; court cards depend on the variant
- Every operation returns exactly one value; errors are "error: ..." strings