FAILURE MAP
← Case archive

FA-82131 / Poker hand evaluation / Open access

Kicker dash range drops its upper end · case 01

"A9s-A6s" counts three classes instead of four.

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

ROOT CAUSE

The dash range uses a half-open interval, so the higher named kicker is excluded.

VERIFIED REPAIR

Both ends of a dash range are inclusive.

Unsuccessful approach: Shifting the start instead drops the lower named kicker.

Case contract

Input: a comma-separated hold'em range string. Tokens: pairs "77", "77+" (up to AA), "TT-77"; non-pairs "AKs"/"AKo"/"AK" (no suffix means both suited and offsuit), "A9s+" (kicker rises to one below the top card), "A9s-A6s" (inclusive). Whitespace around tokens is ignored. Duplicated classes count once. Return total combos: pair 6, suited 4, offsuit 12.

Why this case matters

Range combo counts drive equity, frequency and blocker arithmetic in solvers and trackers.

1 / The failure

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

N = 1
observations = []
def solve(x):
    R='23456789TJQKA'
    classes=set()
    def add(h, suf):
        if h[0]==h[1]: classes.add(h)
        elif suf: classes.add(h+suf)
        else:
            classes.add(h+'s'); classes.add(h+'o')
    for tok in x.split(','):
        tok=tok.strip()
        if not tok: continue
        plus=tok.endswith('+')
        t=tok.rstrip('+')
        if '-' in t:
            a,b=t.split('-')
            if a[0]==a[1]:
                lo,hi=sorted([R.index(a[0]),R.index(b[0])])
                for i in range(lo,hi+1): add(R[i]*2,'')
            else:
                lo,hi=sorted([R.index(a[1]),R.index(b[1])])
                for i in range(lo,hi): add(a[0]+R[i],a[2:])
            continue
        if t[0]==t[1]:
            start=R.index(t[0])
            stop=len(R)-1 if plus else start
            for i in range(start,stop+1): add(R[i]*2,'')
        else:
            top=R.index(t[0]); k=R.index(t[1])
            stop=top-1 if plus else k
            for i in range(k,stop+1): add(t[0]+R[i],t[2:])
    total=0
    for c in classes:
        if c[0]==c[1]: total+=6
        elif c.endswith('s'): total+=4
        else: total+=12
    return total
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('pairs plus', '77+', 48), ('queens plus', 'QQ+', 18), ('suited ace plus', 'A9s+', 20),
  ('offsuit king plus', 'KTo+', 36), ('unsuffixed class', 'AK', 16),
  ('dash kicker range', 'A9s-A6s', 16), ('dash pair range', 'TT-77', 24),
  ('reversed kicker dash', 'K6o-K9o', 48)],
 [('offsuit king plus', 'KTo+', 36), ('unsuffixed class', 'AK', 16),
  ('dash kicker range', 'A9s-A6s', 16), ('dash pair range', 'TT-77', 24),
  ('reversed dash pairs', '77-TT', 24), ('spaced tokens', 'QQ+, AKs', 22),
  ('overlap dedupe', 'AKs,AK', 16), ('reversed kicker dash', 'K6o-K9o', 48)],
 [('dash kicker range', 'A9s-A6s', 16), ('reversed dash pairs', '77-TT', 24),
  ('spaced tokens', 'QQ+, AKs', 22), ('overlap dedupe', 'AKs,AK', 16), ('single pair', '22', 6),
  ('mixed', 'JJ+,AQs+,KQo', 44), ('aces only', 'AA', 6), ('reversed kicker dash', 'K6o-K9o', 48)],
 [('pairs plus', '77+', 48), ('dash kicker range', 'A9s-A6s', 16), ('single pair', '22', 6),
  ('mixed', 'JJ+,AQs+,KQo', 44), ('aces only', 'AA', 6), ('suited connector plus', 'T9s+', 4),
  ('trailing comma', 'KK+,', 12), ('reversed kicker dash', 'K6o-K9o', 48)],
 [('pairs plus', '77+', 48), ('queens plus', 'QQ+', 18), ('suited ace plus', 'A9s+', 20),
  ('offsuit king plus', 'KTo+', 36), ('dash kicker range', 'A9s-A6s', 16),
  ('suited connector plus', 'T9s+', 4), ('trailing comma', 'KK+,', 12),
  ('reversed kicker dash', 'K6o-K9o', 48)]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'error:' + type(exc).__name__
    check(label, actual, 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
pairs plus4848Passed
queens plus1818Passed
suited ace plus2020Passed
offsuit king plus3636Passed
unsuffixed class1616Passed
dash kicker range1216Failed
dash pair range2424Passed
reversed kicker dash3648Failed

SHA-256 / a127492b7f267bf73dc3bcf9d5d361306623b039afda62977f2b221719d1dd7f

2 / The unsuccessful fix

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

N = 1
observations = []
def solve(x):
    R='23456789TJQKA'
    classes=set()
    def add(h, suf):
        if h[0]==h[1]: classes.add(h)
        elif suf: classes.add(h+suf)
        else:
            classes.add(h+'s'); classes.add(h+'o')
    for tok in x.split(','):
        tok=tok.strip()
        if not tok: continue
        plus=tok.endswith('+')
        t=tok.rstrip('+')
        if '-' in t:
            a,b=t.split('-')
            if a[0]==a[1]:
                lo,hi=sorted([R.index(a[0]),R.index(b[0])])
                for i in range(lo,hi+1): add(R[i]*2,'')
            else:
                lo,hi=sorted([R.index(a[1]),R.index(b[1])])
                for i in range(lo+1,hi+1): add(a[0]+R[i],a[2:])
            continue
        if t[0]==t[1]:
            start=R.index(t[0])
            stop=len(R)-1 if plus else start
            for i in range(start,stop+1): add(R[i]*2,'')
        else:
            top=R.index(t[0]); k=R.index(t[1])
            stop=top-1 if plus else k
            for i in range(k,stop+1): add(t[0]+R[i],t[2:])
    total=0
    for c in classes:
        if c[0]==c[1]: total+=6
        elif c.endswith('s'): total+=4
        else: total+=12
    return total
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('pairs plus', '77+', 48), ('queens plus', 'QQ+', 18), ('suited ace plus', 'A9s+', 20),
  ('offsuit king plus', 'KTo+', 36), ('unsuffixed class', 'AK', 16),
  ('dash kicker range', 'A9s-A6s', 16), ('dash pair range', 'TT-77', 24),
  ('reversed kicker dash', 'K6o-K9o', 48)],
 [('offsuit king plus', 'KTo+', 36), ('unsuffixed class', 'AK', 16),
  ('dash kicker range', 'A9s-A6s', 16), ('dash pair range', 'TT-77', 24),
  ('reversed dash pairs', '77-TT', 24), ('spaced tokens', 'QQ+, AKs', 22),
  ('overlap dedupe', 'AKs,AK', 16), ('reversed kicker dash', 'K6o-K9o', 48)],
 [('dash kicker range', 'A9s-A6s', 16), ('reversed dash pairs', '77-TT', 24),
  ('spaced tokens', 'QQ+, AKs', 22), ('overlap dedupe', 'AKs,AK', 16), ('single pair', '22', 6),
  ('mixed', 'JJ+,AQs+,KQo', 44), ('aces only', 'AA', 6), ('reversed kicker dash', 'K6o-K9o', 48)],
 [('pairs plus', '77+', 48), ('dash kicker range', 'A9s-A6s', 16), ('single pair', '22', 6),
  ('mixed', 'JJ+,AQs+,KQo', 44), ('aces only', 'AA', 6), ('suited connector plus', 'T9s+', 4),
  ('trailing comma', 'KK+,', 12), ('reversed kicker dash', 'K6o-K9o', 48)],
 [('pairs plus', '77+', 48), ('queens plus', 'QQ+', 18), ('suited ace plus', 'A9s+', 20),
  ('offsuit king plus', 'KTo+', 36), ('dash kicker range', 'A9s-A6s', 16),
  ('suited connector plus', 'T9s+', 4), ('trailing comma', 'KK+,', 12),
  ('reversed kicker dash', 'K6o-K9o', 48)]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'error:' + type(exc).__name__
    check(label, actual, 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
pairs plus4848Passed
queens plus1818Passed
suited ace plus2020Passed
offsuit king plus3636Passed
unsuffixed class1616Passed
dash kicker range1216Failed
dash pair range2424Passed
reversed kicker dash3648Failed

SHA-256 / 5b5156da50301e7b5ac6724d384f57ec49412b4f7ae1fb4bb61bacff0836f8d4

3 / The verified repair

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

N = 1
observations = []
def solve(x):
    R='23456789TJQKA'
    classes=set()
    def add(h, suf):
        if h[0]==h[1]: classes.add(h)
        elif suf: classes.add(h+suf)
        else:
            classes.add(h+'s'); classes.add(h+'o')
    for tok in x.split(','):
        tok=tok.strip()
        if not tok: continue
        plus=tok.endswith('+')
        t=tok.rstrip('+')
        if '-' in t:
            a,b=t.split('-')
            if a[0]==a[1]:
                lo,hi=sorted([R.index(a[0]),R.index(b[0])])
                for i in range(lo,hi+1): add(R[i]*2,'')
            else:
                lo,hi=sorted([R.index(a[1]),R.index(b[1])])
                for i in range(lo,hi+1): add(a[0]+R[i],a[2:])
            continue
        if t[0]==t[1]:
            start=R.index(t[0])
            stop=len(R)-1 if plus else start
            for i in range(start,stop+1): add(R[i]*2,'')
        else:
            top=R.index(t[0]); k=R.index(t[1])
            stop=top-1 if plus else k
            for i in range(k,stop+1): add(t[0]+R[i],t[2:])
    total=0
    for c in classes:
        if c[0]==c[1]: total+=6
        elif c.endswith('s'): total+=4
        else: total+=12
    return total
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('pairs plus', '77+', 48), ('queens plus', 'QQ+', 18), ('suited ace plus', 'A9s+', 20),
  ('offsuit king plus', 'KTo+', 36), ('unsuffixed class', 'AK', 16),
  ('dash kicker range', 'A9s-A6s', 16), ('dash pair range', 'TT-77', 24),
  ('reversed kicker dash', 'K6o-K9o', 48)],
 [('offsuit king plus', 'KTo+', 36), ('unsuffixed class', 'AK', 16),
  ('dash kicker range', 'A9s-A6s', 16), ('dash pair range', 'TT-77', 24),
  ('reversed dash pairs', '77-TT', 24), ('spaced tokens', 'QQ+, AKs', 22),
  ('overlap dedupe', 'AKs,AK', 16), ('reversed kicker dash', 'K6o-K9o', 48)],
 [('dash kicker range', 'A9s-A6s', 16), ('reversed dash pairs', '77-TT', 24),
  ('spaced tokens', 'QQ+, AKs', 22), ('overlap dedupe', 'AKs,AK', 16), ('single pair', '22', 6),
  ('mixed', 'JJ+,AQs+,KQo', 44), ('aces only', 'AA', 6), ('reversed kicker dash', 'K6o-K9o', 48)],
 [('pairs plus', '77+', 48), ('dash kicker range', 'A9s-A6s', 16), ('single pair', '22', 6),
  ('mixed', 'JJ+,AQs+,KQo', 44), ('aces only', 'AA', 6), ('suited connector plus', 'T9s+', 4),
  ('trailing comma', 'KK+,', 12), ('reversed kicker dash', 'K6o-K9o', 48)],
 [('pairs plus', '77+', 48), ('queens plus', 'QQ+', 18), ('suited ace plus', 'A9s+', 20),
  ('offsuit king plus', 'KTo+', 36), ('dash kicker range', 'A9s-A6s', 16),
  ('suited connector plus', 'T9s+', 4), ('trailing comma', 'KK+,', 12),
  ('reversed kicker dash', 'K6o-K9o', 48)]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'error:' + type(exc).__name__
    check(label, actual, 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
pairs plus4848Passed
queens plus1818Passed
suited ace plus2020Passed
offsuit king plus3636Passed
unsuffixed class1616Passed
dash kicker range1616Passed
dash pair range2424Passed
reversed kicker dash4848Passed

SHA-256 / 7f2bb6baf0053bb70e7320baaffa31ae7f375d11d9329bc7d7e71fbf6e5bc877

Verification & scope

A deterministic toy model with an explicitly stated contract; card strings are rank+suit (for example "As", "Td"); no claim of conformance to any casino or tournament rulebook. 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:09.427022+00:00.

Case digest / 2e2c9c31ffc8b3879bad0cb21f2d9e9377a4fc9cb8c3b45e170415f0af9c16dd