# src/core/hand_evaluator.gd # Pure functions �?zero Godot dependency # Evaluate: [Card] + current_rank + RuleConfig -> EvaluatedPlay | null class_name HandEvaluator extends RefCounted const _C = preload("res://src/core/constants.gd") const INVALID := -1 class EvaluatedPlay: extends RefCounted var type: int = INVALID var primary_rank: int = 0 var cards: Array[Card] = [] var is_pure_bomb: bool = false func _to_string() -> String: var names := ["SINGLE","PAIR","TRIPLE","TRIPLE+2","STRAIGHT","PAIRS","STEEL","FLUSH","BOMB","ROCKET"] return "EvaluatedPlay(%s, rank=%d, pure=%s)" % [names[type], primary_rank, is_pure_bomb] static func evaluate(cards: Array[Card], current_rank: int, config: RuleConfig) -> EvaluatedPlay: if cards.is_empty(): return null var sorted := cards.duplicate(false) _sort_by_rank_and_suit(sorted) var n := sorted.size() if n == 1: return _make_result(_C.TYPE_SINGLE, sorted[0].rank(), false, sorted) var wilds: Array[Card] = [] var reals: Array[Card] = [] for c in sorted: if c.is_heart() and c.rank() == current_rank: wilds.append(c) else: reals.append(c) var has_wild := wilds.size() > 0 if n == 2: return _eval_two(reals, wilds, has_wild) if n == 3: return _eval_three(reals, wilds, has_wild) if n == 4: var result := _eval_four(reals, wilds, has_wild, current_rank, config) if result != null: return result return _eval_multi(reals, wilds, has_wild, current_rank, n, config) static func _make_result(type: int, primary_rank: int, is_pure: bool, cards1: Array[Card]) -> EvaluatedPlay: var r := EvaluatedPlay.new() r.type = type r.primary_rank = primary_rank r.cards = cards1.duplicate(false) r.is_pure_bomb = is_pure return r static func _eval_two(reals: Array[Card], wilds: Array[Card], has_wild: bool) -> EvaluatedPlay: if not has_wild: if reals.size() == 2 and reals[0].rank() == reals[1].rank(): return _make_result(_C.TYPE_PAIR, reals[0].rank(), true, reals) else: if reals.size() == 1: return _make_result(_C.TYPE_PAIR, reals[0].rank(), false, reals + wilds) return null static func _eval_three(reals: Array[Card], wilds: Array[Card], has_wild: bool) -> EvaluatedPlay: if not has_wild: if reals.size() == 3 and _all_same_rank(reals): return _make_result(_C.TYPE_TRIPLE, reals[0].rank(), true, reals) else: if reals.size() == 2 and reals[0].rank() == reals[1].rank(): return _make_result(_C.TYPE_TRIPLE, reals[0].rank(), false, reals + wilds) if reals.size() == 1 and wilds.size() >= 2: return _make_result(_C.TYPE_TRIPLE, reals[0].rank(), false, reals + wilds.slice(0, 2)) return null static func _eval_four(reals: Array[Card], wilds: Array[Card], has_wild: bool, current_rank: int, config: RuleConfig) -> EvaluatedPlay: var all_cards := reals + wilds if not has_wild and reals.size() == 4: if _all_same_rank(reals): return _make_result(_C.TYPE_BOMB, reals[0].rank(), true, reals) var sj_count := 0 var bj_count := 0 for c in reals: if c.rank() == 15: sj_count += 1 if c.rank() == 16: bj_count += 1 if sj_count == 2 and bj_count == 2: return _make_result(_C.TYPE_ROCKET, 999, true, reals) return null if has_wild: if reals.size() == 3 and _all_same_rank(reals): return _make_result(_C.TYPE_BOMB, reals[0].rank(), false, all_cards) if reals.size() == 2 and reals[0].rank() == reals[1].rank(): return _make_result(_C.TYPE_BOMB, reals[0].rank(), false, all_cards) return null static func _eval_multi(reals: Array[Card], wilds: Array[Card], has_wild: bool, current_rank: int, n: int, config: RuleConfig) -> EvaluatedPlay: if not has_wild: return _eval_pure_multiple(reals, n, config) if n == 5: var result := _eval_triple_plus_two(reals, wilds) if result != null: return result result = _eval_straight(reals, wilds, 5, config) if result != null: return result if n >= 6 and n % 2 == 0: var result := _eval_consecutive_pairs(reals, wilds, n / 2, config) if result != null: return result if n >= 6 and n % 3 == 0: var result := _eval_steel_plate(reals, wilds, n / 3, config) if result != null: return result if n >= 5: var result := _eval_straight(reals, wilds, n, config) if result != null: return result return null static func _eval_pure_multiple(reals: Array[Card], n: int, config: RuleConfig) -> EvaluatedPlay: if n >= 5 and _is_straight(reals, config): return _make_result(_C.TYPE_STRAIGHT, reals[n-1].rank(), true, reals) if n >= 6 and n % 2 == 0 and _is_consecutive_pairs(reals, n / 2): return _make_result(_C.TYPE_CONSECUTIVE_PAIRS, reals[n-1].rank(), true, reals) if n == 5 and _is_triple_plus_two(reals): return _make_result(_C.TYPE_TRIPLE_PLUS_TWO, _get_triple_rank(reals), true, reals) if n >= 6 and n % 3 == 0 and _is_steel_plate(reals): return _make_result(_C.TYPE_STEEL_PLATE, reals[n-1].rank(), true, reals) if n >= 5 and _is_straight_flush(reals, config): return _make_result(_C.TYPE_STRAIGHT_FLUSH, reals[n-1].rank(), true, reals) return null static func _eval_triple_plus_two(reals: Array[Card], wilds: Array[Card]) -> EvaluatedPlay: if reals.size() + wilds.size() != 5: return null if reals.is_empty(): return null var rank_counts := _rank_counts(reals) if rank_counts.values().has(3): return _make_result(_C.TYPE_TRIPLE_PLUS_TWO, _get_triple_rank(reals), wilds.is_empty(), reals + wilds) return null static func _eval_straight(reals: Array[Card], wilds: Array[Card], n: int, config: RuleConfig) -> EvaluatedPlay: if reals.size() + wilds.size() != n: return null var sorted: Array[Card] = [] for rc in reals: sorted.append(rc) _sort_by_rank_and_suit(sorted) var w := wilds.size() var gaps: Array[int] = [] for i in range(sorted.size() - 1): var card_a: Card = sorted[i] var card_b: Card = sorted[i+1] var delta: int = card_b.rank() - card_a.rank() if delta > 1: gaps.append(delta - 1) var total_gaps := 0 for g in gaps: total_gaps += g if total_gaps == w or (total_gaps <= w and w >= total_gaps): var last_card: Card = sorted[sorted.size()-1] var max_rank: int = last_card.rank() if not config.straight_extends_to_ace and max_rank > 14: return null return _make_result(_C.TYPE_STRAIGHT, max_rank, w == 0, reals + wilds) return null static func _eval_consecutive_pairs(reals: Array[Card], wilds: Array[Card], pair_count: int, config: RuleConfig) -> EvaluatedPlay: if reals.size() + wilds.size() != pair_count * 2: return null if wilds.is_empty() and _is_consecutive_pairs(reals, pair_count): return _make_result(_C.TYPE_CONSECUTIVE_PAIRS, reals[reals.size()-1].rank(), true, reals) return null static func _eval_steel_plate(reals: Array[Card], wilds: Array[Card], triple_count: int, config: RuleConfig) -> EvaluatedPlay: if reals.size() + wilds.size() != triple_count * 3: return null if wilds.is_empty() and _is_steel_plate(reals): return _make_result(_C.TYPE_STEEL_PLATE, reals[reals.size()-1].rank(), true, reals) return null static func _all_same_rank(cards: Array[Card]) -> bool: var r := cards[0].rank() for i in range(1, cards.size()): if cards[i].rank() != r: return false return true static func _is_straight(cards: Array[Card], config: RuleConfig) -> bool: for i in range(cards.size() - 1): if cards[i+1].rank() - cards[i].rank() != 1: return false if not config.straight_extends_to_ace and cards[cards.size()-1].rank() > 14: return false return true static func _is_consecutive_pairs(cards: Array[Card], pair_count: int) -> bool: for i in range(0, cards.size(), 2): if cards[i].rank() != cards[i+1].rank(): return false for i in range(0, cards.size() - 2, 2): if cards[i+2].rank() - cards[i].rank() != 1: return false return true static func _is_triple_plus_two(cards: Array[Card]) -> bool: var rank_counts := _rank_counts(cards) return rank_counts.values().has(3) and rank_counts.values().has(2) static func _is_steel_plate(cards: Array[Card]) -> bool: var rank_counts := _rank_counts(cards) for count in rank_counts.values(): if count != 3: return false var ranks := rank_counts.keys() ranks.sort() for i in range(ranks.size() - 1): if ranks[i+1] - ranks[i] != 1: return false return true static func _is_straight_flush(cards: Array[Card], config: RuleConfig) -> bool: if not _is_straight(cards, config): return false var suit := cards[0].suit() for i in range(1, cards.size()): if cards[i].suit() != suit: return false return true static func _rank_counts(cards: Array[Card]) -> Dictionary: var d := {} for c in cards: var r := c.rank() d[r] = d.get(r, 0) + 1 return d static func _get_triple_rank(cards: Array[Card]) -> int: var rc := _rank_counts(cards) for rank in rc: if rc[rank] == 3: return rank return 0 static func _sort_by_rank_and_suit(cards: Array[Card]) -> void: cards.sort_custom(func(a: Card, b: Card): return a.compare_to(b) < 0)