# src/core/rule_engine.gd # Pure functions — zero Godot dependency # Validate play legality and compare plays class_name RuleEngine extends RefCounted const _C = preload("res://src/core/constants.gd") static func can_play( hand: Array, play: HandEvaluator.EvaluatedPlay, table_history: Array, last_player_idx: int, current_rank: int, config: RuleConfig ) -> Dictionary: if play.type == -1: var last_non_pass := _last_non_pass(table_history) if table_history.is_empty() or last_non_pass == null: return _err(_C.ERR_CANNOT_PASS) if last_player_idx == _last_player_idx(table_history): return _err(_C.ERR_CANNOT_PASS) return _ok() for pc in play.cards: if not _card_in_hand(hand, pc): return _err(_C.ERR_CARD_NOT_FOUND) var last_play := _last_non_pass(table_history) if last_play == null: return _ok() if last_player_idx < 0: return _ok() if _last_player_idx(table_history) == last_player_idx: return _ok() var cmp := compare(play, last_play, config) if cmp <= 0: return _err(_C.ERR_INVALID_CARDS) if play.type != last_play.type: if play.type != _C.TYPE_BOMB and play.type != _C.TYPE_ROCKET: return _err(_C.ERR_INVALID_CARDS) return _ok() static func compare(a: HandEvaluator.EvaluatedPlay, b: HandEvaluator.EvaluatedPlay, config: RuleConfig) -> int: if a.type == -1 or b.type == -1: return 0 if a.type == _C.TYPE_ROCKET and b.type == _C.TYPE_ROCKET: return 0 if a.type == _C.TYPE_ROCKET: return 1 if b.type == _C.TYPE_ROCKET: return -1 if a.type == _C.TYPE_BOMB and b.type != _C.TYPE_BOMB: return 1 if b.type == _C.TYPE_BOMB and a.type != _C.TYPE_BOMB: return -1 if a.type == _C.TYPE_BOMB and b.type == _C.TYPE_BOMB: return compare_bombs(a, b, config) if a.type == b.type: if a.primary_rank > b.primary_rank: return 1 if a.primary_rank < b.primary_rank: return -1 return 0 return 0 static func compare_bombs(a: HandEvaluator.EvaluatedPlay, b: HandEvaluator.EvaluatedPlay, config: RuleConfig) -> int: if a.is_pure_bomb and not b.is_pure_bomb: return 1 if b.is_pure_bomb and not a.is_pure_bomb: return -1 if config.bomb_compare_priority == RuleConfig.BOMB_BY_COUNT: if a.cards.size() > b.cards.size(): return 1 if b.cards.size() > a.cards.size(): return -1 if a.primary_rank > b.primary_rank: return 1 if b.primary_rank > a.primary_rank: return -1 if a.cards.size() > b.cards.size(): return 1 if b.cards.size() > a.cards.size(): return -1 return 0 static func _card_in_hand(hand: Array, card: Card) -> bool: for c in hand: if c.card_id == card.card_id: return true return false static func _last_non_pass(table: Array) -> HandEvaluator.EvaluatedPlay: for i in range(table.size() - 1, -1, -1): if table[i].type != -1: return table[i] return null static func _last_player_idx(table: Array) -> int: for i in range(table.size() - 1, -1, -1): if table[i].type != -1: return i % 4 return -1 static func _ok() -> Dictionary: return {"ok": true, "error_code": _C.RESULT_OK, "data": null} static func _err(code: int) -> Dictionary: return {"ok": false, "error_code": code, "data": null}