FAILURE MAP
← Case archive

FA-83366 / Card game rule engines / Open access

Unbroken-hearts lead restriction also forbids the queen of spades · case 01

A player cannot lead QS before hearts are broken.

Verified by executionVariant 1 · 8 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

The lead filter reuses the penalty-card predicate.

VERIFIED REPAIR

Before hearts are broken only heart leads are restricted.

Unsuccessful approach: Returning the filtered list unconditionally leaves an all-heart hand with no legal lead.

Case contract

Input [hand, trick_so_far, hearts_broken, first_trick]. Leading: on the first trick the holder of 2C must lead it; hearts may not be led before they are broken unless the hand is all hearts (QS may be led). Following: follow the led suit if possible; on the first trick a void player may not discard hearts or QS unless nothing else is held. Return legal cards in hand order.

Why this case matters

Card-game engines, scoring apps and online tables apply this rule automatically on every hand.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(x):
    hand, trick, broken, first = x
    def penalty(c):
        return c[1] == 'H' or c == 'QS'
    if not trick:
        if first and '2C' in hand:
            return ['2C']
        if not broken:
            safe = [c for c in hand if not penalty(c)]
            if safe:
                return safe
        return list(hand)
    suit = trick[0][1]
    follow = [c for c in hand if c[1] == suit]
    if follow:
        return follow
    if first:
        clean = [c for c in hand if not penalty(c)]
        if clean:
            return clean
    return list(hand)
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[['6H', 'AH', 'JC'], ['7S', 'KC'], False, False], ['6H', 'AH', 'JC']], [[['5H', '6C', '7C', '9C', 'KD', 'QC', 'TS'], ['3S', '8S', '2S'], False, True], ['TS']], [[['2C', 'JS'], [], True, True], ['2C']], [[['AS', 'TH'], [], False, True], ['AS']], [[['2C', '8C', 'AS', 'KH'], [], True, False], ['2C', '8C', 'AS', 'KH']], [[['2H', '3C', '3H', 'QC'], [], True, False], ['2H', '3C', '3H', 'QC']], [[['3D', '9H', 'QS'], [], False, False], ['3D', 'QS']], [[['3H'], [], False, False], ['3H']]], [[[['2S', 'KS', 'TH'], ['JC', '2C'], False, True], ['2S', 'KS']], [[['2S'], ['QS'], False, False], ['2S']], [[['2H', '6C', '7H', '9C', '9S'], ['QC', '2C', '6H'], False, False], ['6C', '9C']], [[['3H', '5D', '9D'], ['3C'], True, False], ['3H', '5D', '9D']], [[['AH', 'JH', 'TH'], ['9C', 'TS'], True, False], ['AH', 'JH', 'TH']], [[['2H', '6S', '8D', 'AC', 'JD', 'QH'], ['4H', 'KC', '3S'], True, True], ['2H', 'QH']], [[['2H', '4C', 'AH', 'AS', 'KC', 'QS', 'TS'], [], False, False], ['4C', 'AS', 'KC', 'QS', 'TS']], [[['3H', '5H'], [], False, False], ['3H', '5H']]], [[[['5C'], [], False, True], ['5C']], [[['5D', 'JH'], ['QH', 'KC', '8S'], True, False], ['JH']], [[['4H'], [], True, True], ['4H']], [[['3D', '5C'], [], True, False], ['3D', '5C']], [[['2H', '5H'], ['KS', '9C', '7H'], False, False], ['2H', '5H']], [[['8C', 'AC', 'KD', 'TS'], [], False, True], ['8C', 'AC', 'KD', 'TS']], [[['4H', '6D', '7C', '7S', '9D', 'QS', 'TD'], [], False, True], ['6D', '7C', '7S', '9D', 'QS', 'TD']], [[['4H'], [], False, False], ['4H']]], [[[['2H', '6C', '6D', '7C', 'KS', 'QC'], ['3C', '7D'], True, False], ['6C', '7C', 'QC']], [[['AC'], ['2C', '8D'], False, False], ['AC']], [[['2S', '6D'], [], True, False], ['2S', '6D']], [[['5D', 'AS'], ['5S', '3C'], False, False], ['AS']], [[['3C', '4S', '6S', '7D', '8H', 'AS', 'KC', 'KH'], [], False, False], ['3C', '4S', '6S', '7D', 'AS', 'KC']], [[['2H', '3D', '5H', '9D', 'JC', 'KS', 'QD'], [], False, False], ['3D', '9D', 'JC', 'KS', 'QD']], [[['7H'], [], False, False], ['7H']], [[['QS', 'AD'], [], False, False], ['QS', 'AD']]], [[[['4D', '6S', '9C', 'JD'], ['9H', 'AS', 'KC'], False, True], ['4D', '6S', '9C', 'JD']], [[['3S', '7D'], [], False, True], ['3S', '7D']], [[['2D', '3H', '3S', '4S', '6D', 'QC', 'QH'], ['6C', 'AH'], False, False], ['QC']], [[['3C', '6D', 'TH'], [], False, False], ['3C', '6D']], [[['3D', '5C', '6S', '7H', 'KH', 'TH', 'TS'], [], False, False], ['3D', '5C', '6S', 'TS']], [[['7D', '8H', 'AD', 'AS'], ['AH', '4H', '9C'], True, True], ['8H']], [[['3C', '3S', '4H', 'KC', 'QS'], [], False, False], ['3C', '3S', 'KC', 'QS']], [[['3H'], [], False, False], ['3H']]]]
for i, (args, expected) in enumerate(fixtures[N-1]):
    check("legal plays case %d" % i, solve(args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
legal plays case 0['6H', 'AH', 'JC']['6H', 'AH', 'JC']Passed
legal plays case 1['TS']['TS']Passed
legal plays case 2['2C']['2C']Passed
legal plays case 3['AS']['AS']Passed
legal plays case 4['2C', '8C', 'AS', 'KH']['2C', '8C', 'AS', 'KH']Passed
legal plays case 5['2H', '3C', '3H', 'QC']['2H', '3C', '3H', 'QC']Passed
legal plays case 6['3D']['3D', 'QS']Failed
legal plays case 7['3H']['3H']Passed

SHA-256 / c199894d5b9cf2ab23f6bfc98526324e571367c74764009fad914ac5329b9b8f

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(x):
    hand, trick, broken, first = x
    def penalty(c):
        return c[1] == 'H' or c == 'QS'
    if not trick:
        if first and '2C' in hand:
            return ['2C']
        if not broken:
            safe = [c for c in hand if c[1] != 'H']
            return safe
        return list(hand)
    suit = trick[0][1]
    follow = [c for c in hand if c[1] == suit]
    if follow:
        return follow
    if first:
        clean = [c for c in hand if not penalty(c)]
        if clean:
            return clean
    return list(hand)
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[['6H', 'AH', 'JC'], ['7S', 'KC'], False, False], ['6H', 'AH', 'JC']], [[['5H', '6C', '7C', '9C', 'KD', 'QC', 'TS'], ['3S', '8S', '2S'], False, True], ['TS']], [[['2C', 'JS'], [], True, True], ['2C']], [[['AS', 'TH'], [], False, True], ['AS']], [[['2C', '8C', 'AS', 'KH'], [], True, False], ['2C', '8C', 'AS', 'KH']], [[['2H', '3C', '3H', 'QC'], [], True, False], ['2H', '3C', '3H', 'QC']], [[['3D', '9H', 'QS'], [], False, False], ['3D', 'QS']], [[['3H'], [], False, False], ['3H']]], [[[['2S', 'KS', 'TH'], ['JC', '2C'], False, True], ['2S', 'KS']], [[['2S'], ['QS'], False, False], ['2S']], [[['2H', '6C', '7H', '9C', '9S'], ['QC', '2C', '6H'], False, False], ['6C', '9C']], [[['3H', '5D', '9D'], ['3C'], True, False], ['3H', '5D', '9D']], [[['AH', 'JH', 'TH'], ['9C', 'TS'], True, False], ['AH', 'JH', 'TH']], [[['2H', '6S', '8D', 'AC', 'JD', 'QH'], ['4H', 'KC', '3S'], True, True], ['2H', 'QH']], [[['2H', '4C', 'AH', 'AS', 'KC', 'QS', 'TS'], [], False, False], ['4C', 'AS', 'KC', 'QS', 'TS']], [[['3H', '5H'], [], False, False], ['3H', '5H']]], [[[['5C'], [], False, True], ['5C']], [[['5D', 'JH'], ['QH', 'KC', '8S'], True, False], ['JH']], [[['4H'], [], True, True], ['4H']], [[['3D', '5C'], [], True, False], ['3D', '5C']], [[['2H', '5H'], ['KS', '9C', '7H'], False, False], ['2H', '5H']], [[['8C', 'AC', 'KD', 'TS'], [], False, True], ['8C', 'AC', 'KD', 'TS']], [[['4H', '6D', '7C', '7S', '9D', 'QS', 'TD'], [], False, True], ['6D', '7C', '7S', '9D', 'QS', 'TD']], [[['4H'], [], False, False], ['4H']]], [[[['2H', '6C', '6D', '7C', 'KS', 'QC'], ['3C', '7D'], True, False], ['6C', '7C', 'QC']], [[['AC'], ['2C', '8D'], False, False], ['AC']], [[['2S', '6D'], [], True, False], ['2S', '6D']], [[['5D', 'AS'], ['5S', '3C'], False, False], ['AS']], [[['3C', '4S', '6S', '7D', '8H', 'AS', 'KC', 'KH'], [], False, False], ['3C', '4S', '6S', '7D', 'AS', 'KC']], [[['2H', '3D', '5H', '9D', 'JC', 'KS', 'QD'], [], False, False], ['3D', '9D', 'JC', 'KS', 'QD']], [[['7H'], [], False, False], ['7H']], [[['QS', 'AD'], [], False, False], ['QS', 'AD']]], [[[['4D', '6S', '9C', 'JD'], ['9H', 'AS', 'KC'], False, True], ['4D', '6S', '9C', 'JD']], [[['3S', '7D'], [], False, True], ['3S', '7D']], [[['2D', '3H', '3S', '4S', '6D', 'QC', 'QH'], ['6C', 'AH'], False, False], ['QC']], [[['3C', '6D', 'TH'], [], False, False], ['3C', '6D']], [[['3D', '5C', '6S', '7H', 'KH', 'TH', 'TS'], [], False, False], ['3D', '5C', '6S', 'TS']], [[['7D', '8H', 'AD', 'AS'], ['AH', '4H', '9C'], True, True], ['8H']], [[['3C', '3S', '4H', 'KC', 'QS'], [], False, False], ['3C', '3S', 'KC', 'QS']], [[['3H'], [], False, False], ['3H']]]]
for i, (args, expected) in enumerate(fixtures[N-1]):
    check("legal plays case %d" % i, solve(args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
legal plays case 0['6H', 'AH', 'JC']['6H', 'AH', 'JC']Passed
legal plays case 1['TS']['TS']Passed
legal plays case 2['2C']['2C']Passed
legal plays case 3['AS']['AS']Passed
legal plays case 4['2C', '8C', 'AS', 'KH']['2C', '8C', 'AS', 'KH']Passed
legal plays case 5['2H', '3C', '3H', 'QC']['2H', '3C', '3H', 'QC']Passed
legal plays case 6['3D', 'QS']['3D', 'QS']Passed
legal plays case 7[]['3H']Failed

SHA-256 / 7efb9f560bf643e5d5e48f9647a3c060f6fa12a94adf3a32749c2a0d433c22d0

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(x):
    hand, trick, broken, first = x
    def penalty(c):
        return c[1] == 'H' or c == 'QS'
    if not trick:
        if first and '2C' in hand:
            return ['2C']
        if not broken:
            safe = [c for c in hand if c[1] != 'H']
            if safe:
                return safe
        return list(hand)
    suit = trick[0][1]
    follow = [c for c in hand if c[1] == suit]
    if follow:
        return follow
    if first:
        clean = [c for c in hand if not penalty(c)]
        if clean:
            return clean
    return list(hand)
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[['6H', 'AH', 'JC'], ['7S', 'KC'], False, False], ['6H', 'AH', 'JC']], [[['5H', '6C', '7C', '9C', 'KD', 'QC', 'TS'], ['3S', '8S', '2S'], False, True], ['TS']], [[['2C', 'JS'], [], True, True], ['2C']], [[['AS', 'TH'], [], False, True], ['AS']], [[['2C', '8C', 'AS', 'KH'], [], True, False], ['2C', '8C', 'AS', 'KH']], [[['2H', '3C', '3H', 'QC'], [], True, False], ['2H', '3C', '3H', 'QC']], [[['3D', '9H', 'QS'], [], False, False], ['3D', 'QS']], [[['3H'], [], False, False], ['3H']]], [[[['2S', 'KS', 'TH'], ['JC', '2C'], False, True], ['2S', 'KS']], [[['2S'], ['QS'], False, False], ['2S']], [[['2H', '6C', '7H', '9C', '9S'], ['QC', '2C', '6H'], False, False], ['6C', '9C']], [[['3H', '5D', '9D'], ['3C'], True, False], ['3H', '5D', '9D']], [[['AH', 'JH', 'TH'], ['9C', 'TS'], True, False], ['AH', 'JH', 'TH']], [[['2H', '6S', '8D', 'AC', 'JD', 'QH'], ['4H', 'KC', '3S'], True, True], ['2H', 'QH']], [[['2H', '4C', 'AH', 'AS', 'KC', 'QS', 'TS'], [], False, False], ['4C', 'AS', 'KC', 'QS', 'TS']], [[['3H', '5H'], [], False, False], ['3H', '5H']]], [[[['5C'], [], False, True], ['5C']], [[['5D', 'JH'], ['QH', 'KC', '8S'], True, False], ['JH']], [[['4H'], [], True, True], ['4H']], [[['3D', '5C'], [], True, False], ['3D', '5C']], [[['2H', '5H'], ['KS', '9C', '7H'], False, False], ['2H', '5H']], [[['8C', 'AC', 'KD', 'TS'], [], False, True], ['8C', 'AC', 'KD', 'TS']], [[['4H', '6D', '7C', '7S', '9D', 'QS', 'TD'], [], False, True], ['6D', '7C', '7S', '9D', 'QS', 'TD']], [[['4H'], [], False, False], ['4H']]], [[[['2H', '6C', '6D', '7C', 'KS', 'QC'], ['3C', '7D'], True, False], ['6C', '7C', 'QC']], [[['AC'], ['2C', '8D'], False, False], ['AC']], [[['2S', '6D'], [], True, False], ['2S', '6D']], [[['5D', 'AS'], ['5S', '3C'], False, False], ['AS']], [[['3C', '4S', '6S', '7D', '8H', 'AS', 'KC', 'KH'], [], False, False], ['3C', '4S', '6S', '7D', 'AS', 'KC']], [[['2H', '3D', '5H', '9D', 'JC', 'KS', 'QD'], [], False, False], ['3D', '9D', 'JC', 'KS', 'QD']], [[['7H'], [], False, False], ['7H']], [[['QS', 'AD'], [], False, False], ['QS', 'AD']]], [[[['4D', '6S', '9C', 'JD'], ['9H', 'AS', 'KC'], False, True], ['4D', '6S', '9C', 'JD']], [[['3S', '7D'], [], False, True], ['3S', '7D']], [[['2D', '3H', '3S', '4S', '6D', 'QC', 'QH'], ['6C', 'AH'], False, False], ['QC']], [[['3C', '6D', 'TH'], [], False, False], ['3C', '6D']], [[['3D', '5C', '6S', '7H', 'KH', 'TH', 'TS'], [], False, False], ['3D', '5C', '6S', 'TS']], [[['7D', '8H', 'AD', 'AS'], ['AH', '4H', '9C'], True, True], ['8H']], [[['3C', '3S', '4H', 'KC', 'QS'], [], False, False], ['3C', '3S', 'KC', 'QS']], [[['3H'], [], False, False], ['3H']]]]
for i, (args, expected) in enumerate(fixtures[N-1]):
    check("legal plays case %d" % i, solve(args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
legal plays case 0['6H', 'AH', 'JC']['6H', 'AH', 'JC']Passed
legal plays case 1['TS']['TS']Passed
legal plays case 2['2C']['2C']Passed
legal plays case 3['AS']['AS']Passed
legal plays case 4['2C', '8C', 'AS', 'KH']['2C', '8C', 'AS', 'KH']Passed
legal plays case 5['2H', '3C', '3H', 'QC']['2H', '3C', '3H', 'QC']Passed
legal plays case 6['3D', 'QS']['3D', 'QS']Passed
legal plays case 7['3H']['3H']Passed

SHA-256 / 9de8bba53bdbb9ee838a7242a2914ca7a3c7583a16fa7df2c14238baa32d87a6

Verification & scope

A bounded toy rule contract stated explicitly in the contract field; cards are two-character codes (rank, suit). Not a complete implementation of any published rulebook or casino table rules. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.

Observations recorded using Python 3.12.14 at 2026-09-29T14:50:20.832696+00:00.

Case digest / e564d4dd3f7a123a3b19146e1a1ebc969820993113b0ff71c7285f2b7cc02b18