109 lines
3.4 KiB
GDScript
109 lines
3.4 KiB
GDScript
# 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}
|