# src/core/move_generator.gd # Pure functions — zero Godot dependency # Generate all legal plays from a hand class_name MoveGenerator extends RefCounted const MAX_ENUM_NODES := 10000 const BEAM_WIDTH := 50 static func generate(hand: Array[Card], current_rank: int, config: RuleConfig) -> Array[HandEvaluator.EvaluatedPlay]: var results: Array[HandEvaluator.EvaluatedPlay] = [] var pass_play := HandEvaluator.EvaluatedPlay.new() pass_play.type = -1 pass_play.primary_rank = 0 results.append(pass_play) if hand.is_empty(): return results var sorted := hand.duplicate(false) _sort(sorted) var seen_ranks := {} for c in sorted: var rk := c.rank() if seen_ranks.get(rk, 0) < 4: var ep := HandEvaluator.evaluate([c], current_rank, config) if ep != null and ep.type != HandEvaluator.INVALID: if not _contains_duplicate(results, ep): results.append(ep) seen_ranks[rk] = seen_ranks.get(rk, 0) + 1 _gen_pairs(sorted, results, current_rank, config) _gen_triples(sorted, results, current_rank, config) _gen_bombs(sorted, results, current_rank, config) _gen_straights(sorted, results, current_rank, config) _gen_rocket(sorted, results, current_rank, config) if results.size() > MAX_ENUM_NODES: results = results.slice(0, MAX_ENUM_NODES) return results static func _gen_pairs(sorted: Array[Card], results: Array[HandEvaluator.EvaluatedPlay], current_rank: int, config: RuleConfig) -> void: var rank_counts := {} for c in sorted: var rk := c.rank() if not rank_counts.has(rk): rank_counts[rk] = [] rank_counts[rk].append(c) for rk in rank_counts: var cards: Array = rank_counts[rk] if cards.size() >= 2: var ep := HandEvaluator.evaluate(cards.slice(0, 2), current_rank, config) if ep != null: results.append(ep) static func _gen_triples(sorted: Array[Card], results: Array[HandEvaluator.EvaluatedPlay], current_rank: int, config: RuleConfig) -> void: var rank_counts := {} for c in sorted: var rk := c.rank() if not rank_counts.has(rk): rank_counts[rk] = [] rank_counts[rk].append(c) for rk in rank_counts: var cards: Array = rank_counts[rk] if cards.size() >= 3: var ep := HandEvaluator.evaluate(cards.slice(0, 3), current_rank, config) if ep != null: results.append(ep) if cards.size() >= 3: _gen_triple_plus_kickers(sorted, cards.slice(0, 3), results, current_rank, config) static func _gen_triple_plus_kickers(hand: Array[Card], triple: Array[Card], results: Array[HandEvaluator.EvaluatedPlay], current_rank: int, config: RuleConfig) -> void: var remaining: Array[Card] = [] for c in hand: if not _card_in(triple, c): remaining.append(c) var pairs := _find_pairs(remaining) for pair in pairs: var ep := HandEvaluator.evaluate(triple + pair, current_rank, config) if ep != null: results.append(ep) static func _gen_bombs(sorted: Array[Card], results: Array[HandEvaluator.EvaluatedPlay], current_rank: int, config: RuleConfig) -> void: var rank_counts := {} for c in sorted: var rk := c.rank() if not rank_counts.has(rk): rank_counts[rk] = [] rank_counts[rk].append(c) for rk in rank_counts: var cards: Array = rank_counts[rk] if cards.size() >= 4: var ep := HandEvaluator.evaluate(cards.slice(0, 4), current_rank, config) if ep != null: results.append(ep) for count in range(5, cards.size() + 1): var ep := HandEvaluator.evaluate(cards.slice(0, count), current_rank, config) if ep != null: results.append(ep) static func _gen_straights(sorted: Array[Card], results: Array[HandEvaluator.EvaluatedPlay], current_rank: int, config: RuleConfig) -> void: var unique := _unique_ranks(sorted) for start in unique: for length in range(5, 13): var needed: Array[int] = [] for r in range(start, start + length): needed.append(r) var found := _pick_consecutive(sorted, needed) if found.size() == length: var ep := HandEvaluator.evaluate(found, current_rank, config) if ep != null: results.append(ep) static func _gen_rocket(sorted: Array[Card], results: Array[HandEvaluator.EvaluatedPlay], current_rank: int, config: RuleConfig) -> void: var sj_indices: Array[int] = [] var bj_indices: Array[int] = [] for i in range(sorted.size()): if sorted[i].rank() == 15: sj_indices.append(i) if sorted[i].rank() == 16: bj_indices.append(i) if sj_indices.size() >= 2 and bj_indices.size() >= 2: var cards: Array[Card] = [] for idx in sj_indices.slice(0, 2) + bj_indices.slice(0, 2): cards.append(sorted[idx]) var ep := HandEvaluator.evaluate(cards, current_rank, config) if ep != null: results.append(ep) static func _find_pairs(hand: Array[Card]) -> Array: var result := [] var rank_groups := {} for c in hand: var rk := c.rank() if not rank_groups.has(rk): rank_groups[rk] = [] rank_groups[rk].append(c) for rk in rank_groups: if rank_groups[rk].size() >= 2: result.append(rank_groups[rk].slice(0, 2)) return result static func _card_in(cards: Array[Card], target: Card) -> bool: for c in cards: if c.card_id == target.card_id: return true return false static func _unique_ranks(hand: Array[Card]) -> Array: var seen := {} var result: Array = [] for c in hand: var rk := c.rank() if not seen.has(rk): seen[rk] = true result.append(rk) result.sort() return result static func _pick_consecutive(hand: Array[Card], needed: Array[int]) -> Array[Card]: var result: Array[Card] = [] var used := {} for target in needed: var found := false for c in hand: if c.rank() == target and not used.has(c.card_id): result.append(c) used[c.card_id] = true found = true break if not found: return [] return result static func _contains_duplicate(results: Array[HandEvaluator.EvaluatedPlay], ep: HandEvaluator.EvaluatedPlay) -> bool: for existing in results: if existing.type == ep.type and existing.primary_rank == ep.primary_rank: if existing.cards.size() == ep.cards.size(): return true return false static func _sort(cards: Array[Card]) -> void: cards.sort_custom(func(a: Card, b: Card): return a.compare_to(b) < 0)