diff --git a/src/core/actions.gd b/src/core/actions.gd new file mode 100644 index 0000000..8cc220c --- /dev/null +++ b/src/core/actions.gd @@ -0,0 +1,40 @@ +# src/core/actions.gd +class_name Actions +extends RefCounted + +class Action: + extends RefCounted + var player_idx: int + var action_type: String + var cards: Array = [] + var seq_id: int = 0 + var nonce: int = 0 + var timestamp: int = 0 + + func _to_string() -> String: + return "Action(seq=%d, player=%d, type=%s, cards=%d)" % [seq_id, player_idx, action_type, cards.size()] + +class GameStateSnapshot: + extends RefCounted + var game_state: Dictionary + +class Team: + extends RefCounted + var team_id: int + var player_indices: Array[int] = [] + var score: int = 0 + var current_level: int = 2 + + static func create_team(team_id: int, p1: int, p2: int) -> Team: + var t := Team.new() + t.team_id = team_id + t.player_indices = [p1, p2] + return t + + func teammate_of(player_idx: int) -> int: + if player_indices[0] == player_idx: + return player_indices[1] + return player_indices[0] + + func contains(player_idx: int) -> bool: + return player_idx in player_indices diff --git a/src/core/game_state.gd b/src/core/game_state.gd new file mode 100644 index 0000000..ff2b29c --- /dev/null +++ b/src/core/game_state.gd @@ -0,0 +1,114 @@ +# src/core/game_state.gd +class_name GameState +extends RefCounted + +enum Phase { INIT, DEAL, TRIBUTE, PLAY, LEVEL_UP, GAME_OVER } + +const _Actions = preload("res://src/core/actions.gd") + +var phase: int = Phase.INIT +var round_seq: int = 0 +var current_rank: int = 2 +var rule_config: RuleConfig +var teams: Array = [] +var player_hands: Array = [[], [], [], []] +var player_names: Array[String] = ["Player", "AI-1", "AI-2", "AI-3"] +var player_human: Array[bool] = [true, false, false, false] +var round: Round +var action_log: Array = [] +var seed: int = 0 +var finished_players: Array[int] = [] +var current_winner_team: int = -1 +var game_end_reason: String = "" + +static func create(config: RuleConfig, seed_: int = -1) -> GameState: + var gs := GameState.new() + gs.rule_config = config + if seed_ >= 0: + gs.seed = seed_ + else: + gs.seed = Time.get_unix_time_from_system() as int + gs.teams = [_Actions.Team.create_team(0, 0, 2), _Actions.Team.create_team(1, 1, 3)] + gs.round = Round.new() + return gs + +func get_team(player_idx: int) -> Actions.Team: + for t in teams: + if t.contains(player_idx): + return t + return null + +func get_partner(player_idx: int) -> int: + var t := get_team(player_idx) + if t != null: + return t.teammate_of(player_idx) + return -1 + +func get_hand(player_idx: int) -> Array: + return player_hands[player_idx] + +func remove_cards_from_hand(player_idx: int, cards: Array) -> void: + var hand: Array = player_hands[player_idx] + var to_remove := {} + for c in cards: + to_remove[c.card_id] = true + var new_hand: Array = [] + for c in hand: + if not to_remove.has(c.card_id): + new_hand.append(c) + player_hands[player_idx] = new_hand + +func add_cards_to_hand(player_idx: int, cards: Array) -> void: + for c in cards: + player_hands[player_idx].append(c) + +func deal_cards(deck: Deck) -> void: + for i in range(4): + player_hands[i] = deck.deal(27) + +func is_player_finished(player_idx: int) -> bool: + return player_idx in finished_players + +func add_finished_player(player_idx: int) -> void: + if not is_player_finished(player_idx): + finished_players.append(player_idx) + +func get_rank_for_player(player_idx: int) -> int: + var idx := finished_players.find(player_idx) + if idx >= 0: + return idx + 1 + return 0 + +func all_hands_empty() -> bool: + var empty_count := 0 + for i in range(4): + if player_hands[i].is_empty(): + empty_count += 1 + return empty_count >= 3 + +func to_packed_snapshot(for_player: int) -> Dictionary: + return { + "phase": phase, + "current_rank": current_rank, + "own_hand": _pack_hand(player_hands[for_player]), + "table": _pack_table(round.table), + "finished": finished_players.duplicate(), + "team_scores": [teams[0].score, teams[1].score] + } + +func _pack_hand(hand: Array) -> Array: + var result := [] + for c in hand: + result.append(c.to_packed()) + return result + +func _pack_table(table: Array) -> Array: + var result := [] + for ep in table: + var d := {"type": ep.type, "primary_rank": ep.primary_rank, "is_pure": ep.is_pure_bomb} + var cards_packed := [] + for c in ep.cards: + cards_packed.append(c.to_packed()) + d["cards"] = cards_packed + result.append(d) + return result diff --git a/src/core/round.gd b/src/core/round.gd new file mode 100644 index 0000000..1518f4d --- /dev/null +++ b/src/core/round.gd @@ -0,0 +1,41 @@ +# src/core/round.gd +class_name Round +extends RefCounted + +var active_player_idx: int = 0 +var pass_count: int = 0 +var table: Array = [] +var action_seq: int = 0 +var is_cleared: bool = false + +func can_pass() -> bool: + return not table.is_empty() + +func add_play(play: HandEvaluator.EvaluatedPlay, player_idx: int) -> void: + table.append(play) + if play.type == -1: + pass_count += 1 + else: + pass_count = 0 + if pass_count >= 3: + is_cleared = true + else: + is_cleared = false + +func next_player() -> int: + return (active_player_idx + 1) % 4 + +func last_non_pass_player() -> int: + for i in range(table.size() - 1, -1, -1): + if table[i].type != -1: + return i % 4 + return -1 + +func next_seq() -> int: + action_seq += 1 + return action_seq + +func reset_for_new_round() -> void: + table.clear() + pass_count = 0 + is_cleared = false diff --git a/src/core/rule_engine.gd b/src/core/rule_engine.gd new file mode 100644 index 0000000..efe2453 --- /dev/null +++ b/src/core/rule_engine.gd @@ -0,0 +1,108 @@ +# 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}