diff --git a/src/autoload/audio_manager.gd b/src/autoload/audio_manager.gd new file mode 100644 index 0000000..cd563c7 --- /dev/null +++ b/src/autoload/audio_manager.gd @@ -0,0 +1,20 @@ +# src/autoload/audio_manager.gd +extends Node + +func play_card_place() -> void: + pass + +func play_bomb() -> void: + pass + +func play_pass() -> void: + pass + +func play_victory() -> void: + pass + +func play_tribute() -> void: + pass + +func set_muted(muted: bool) -> void: + pass diff --git a/src/autoload/config.gd b/src/autoload/config.gd new file mode 100644 index 0000000..9ef0e51 --- /dev/null +++ b/src/autoload/config.gd @@ -0,0 +1,18 @@ +# src/autoload/config.gd +extends Node + +var rule_config: RuleConfig = RuleConfig.standard() +var enable_ai_debug: bool = false +var enable_training_hints: bool = true +var enable_sound: bool = true +var low_perf_mode: bool = false +var turn_timeout_sec: float = 30.0 +var language: String = "zh_cn" + +func reset_to_defaults() -> void: + rule_config = RuleConfig.standard() + enable_ai_debug = false + enable_training_hints = true + enable_sound = true + low_perf_mode = false + turn_timeout_sec = 30.0 diff --git a/src/autoload/event_bus.gd b/src/autoload/event_bus.gd new file mode 100644 index 0000000..a8f580e --- /dev/null +++ b/src/autoload/event_bus.gd @@ -0,0 +1,11 @@ +# src/autoload/event_bus.gd +extends Node + +signal player_played_cards(player_idx: int, play_type: int, cards: Array) +signal bomb_detonated(player_idx: int, rank: int) +signal tribute_triggered(from_idx: int, to_idx: int) +signal round_end() +signal game_over(winner_team: int, reason: String) +signal player_finished(player_idx: int, position: int) +signal turn_changed(player_idx: int) +signal table_cleared(player_idx: int) diff --git a/src/game/game_controller.gd b/src/game/game_controller.gd new file mode 100644 index 0000000..604fef7 --- /dev/null +++ b/src/game/game_controller.gd @@ -0,0 +1,122 @@ +# src/game/game_controller.gd +# Glue layer: connects core logic to UI and AI + +class_name GameController +extends Node + +signal state_changed() +signal turn_ready(player_idx: int, is_human: bool) +signal game_ended(winner_team: int, reason: String) + +var game_state: GameState +var ai_players: Dictionary = {} +var is_processing: bool = false + +func start_game(config: RuleConfig, human_idx: int = 0, seed_: int = -1) -> void: + game_state = GameState.create(config, seed_) + game_state.player_human[human_idx] = true + for i in range(4): + if not game_state.player_human[i]: + ai_players[i] = L2RuleAI.new() + var deck := Deck.create(game_state.seed) + game_state.deal_cards(deck) + game_state.phase = GameState.Phase.PLAY + game_state.round.active_player_idx = 0 + state_changed.emit() + +func handle_human_play(cards: Array) -> Dictionary: + if is_processing: + return {"ok": false, "error_code": 1, "data": null} + var hand := game_state.get_hand(game_state.round.active_player_idx) + var play := HandEvaluator.evaluate(cards, game_state.current_rank, game_state.rule_config) + if play == null or play.type == -1: + return {"ok": false, "error_code": 1, "data": null} + var last_idx := game_state.round.last_non_pass_player() + var result := RuleEngine.can_play(hand, play, game_state.round.table, last_idx, game_state.current_rank, game_state.rule_config) + if not result.ok: + return result + _apply_play(game_state.round.active_player_idx, play) + _advance_turn() + return {"ok": true, "error_code": 0, "data": null} + +func handle_human_pass() -> Dictionary: + if is_processing: + return {"ok": false, "error_code": 1, "data": null} + var old_last := game_state.round.last_non_pass_player() + if old_last >= 0 and old_last == game_state.round.active_player_idx: + return {"ok": false, "error_code": 3, "data": null} + var pass := HandEvaluator.EvaluatedPlay.new() + pass.type = -1 + pass.primary_rank = 0 + _apply_play(game_state.round.active_player_idx, pass) + _advance_turn() + return {"ok": true, "error_code": 0, "data": null} + +func _apply_play(player_idx: int, play: HandEvaluator.EvaluatedPlay) -> void: + if play.type != -1: + game_state.remove_cards_from_hand(player_idx, play.cards) + var action := Actions.Action.new() + action.player_idx = player_idx + action.action_type = "PASS" if play.type == -1 else "PLAY" + action.cards = play.cards.duplicate(false) + action.seq_id = game_state.round.next_seq() + action.timestamp = Time.get_unix_time_from_system() + game_state.action_log.append(action) + game_state.round.add_play(play, player_idx) + var hand: Array = game_state.get_hand(player_idx) + if hand.is_empty() and not game_state.is_player_finished(player_idx): + game_state.add_finished_player(player_idx) + if game_state.all_hands_empty(): + _end_game() + +func _advance_turn() -> void: + var hand: Array = game_state.get_hand(game_state.round.active_player_idx) + if hand.is_empty(): + var partner := game_state.get_partner(game_state.round.active_player_idx) + if not game_state.is_player_finished(partner): + game_state.round.active_player_idx = partner + game_state.round.reset_for_new_round() + else: + _next_alive_player() + elif game_state.round.is_cleared: + game_state.round.reset_for_new_round() + else: + _next_alive_player() + var current := game_state.round.active_player_idx + turn_ready.emit(current, game_state.player_human[current]) + if not game_state.player_human[current]: + _trigger_ai(current) + +func _next_alive_player() -> void: + for _i in range(4): + game_state.round.active_player_idx = (game_state.round.active_player_idx + 1) % 4 + if not game_state.is_player_finished(game_state.round.active_player_idx): + return + +func _trigger_ai(player_idx: int) -> void: + var ai := ai_players.get(player_idx) + if ai == null: + return + var hand8: Array = game_state.get_hand(player_idx) + var decision := ai.decide(hand8, game_state.round.table, game_state.current_rank, game_state.rule_config) + if decision.type == -1: + _apply_play(player_idx, decision) + else: + _apply_play(player_idx, decision) + _advance_turn() + +func _end_game() -> void: + game_state.phase = GameState.Phase.GAME_OVER + var team_counts := [0, 0] + for fp in game_state.finished_players: + var t := game_state.get_team(fp) + if t != null: + team_counts[t.team_id] += 1 + if team_counts[0] >= 2: + game_state.current_winner_team = 0 + elif team_counts[1] >= 2: + game_state.current_winner_team = 1 + else: + game_state.current_winner_team = 0 if team_counts[0] > team_counts[1] else 1 + game_state.game_end_reason = "NORMAL" + game_ended.emit(game_state.current_winner_team, "NORMAL") diff --git a/src/game/replay_recorder.gd b/src/game/replay_recorder.gd new file mode 100644 index 0000000..097e5b6 --- /dev/null +++ b/src/game/replay_recorder.gd @@ -0,0 +1,17 @@ +# src/game/replay_recorder.gd +extends Node + +var is_recording: bool = false +var recorded_actions: Array = [] + +func start_recording() -> void: + recorded_actions.clear() + is_recording = true + +func record_action(action: Actions.Action) -> void: + if is_recording: + recorded_actions.append(action) + +func stop_recording() -> String: + is_recording = false + return "replay_recorded" diff --git a/src/game/training_controller.gd b/src/game/training_controller.gd new file mode 100644 index 0000000..64d2323 --- /dev/null +++ b/src/game/training_controller.gd @@ -0,0 +1,16 @@ +# src/game/training_controller.gd +class_name TrainingController +extends GameController + +var _current_hint: HandEvaluator.EvaluatedPlay = null + +func get_hint() -> HandEvaluator.EvaluatedPlay: + var hand := game_state.get_hand(game_state.round.active_player_idx) + if hand.is_empty(): + return null + var ai := L2RuleAI.new() + return ai.decide(hand, game_state.round.table, game_state.current_rank, game_state.rule_config) + +func get_all_legal_moves() -> Array: + var hand := game_state.get_hand(game_state.round.active_player_idx) + return MoveGenerator.generate(hand, game_state.current_rank, game_state.rule_config)