feat: add autoloads, GameController, TrainingController, and ReplayRecorder
This commit is contained in:
20
src/autoload/audio_manager.gd
Normal file
20
src/autoload/audio_manager.gd
Normal file
@@ -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
|
||||||
18
src/autoload/config.gd
Normal file
18
src/autoload/config.gd
Normal file
@@ -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
|
||||||
11
src/autoload/event_bus.gd
Normal file
11
src/autoload/event_bus.gd
Normal file
@@ -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)
|
||||||
122
src/game/game_controller.gd
Normal file
122
src/game/game_controller.gd
Normal file
@@ -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")
|
||||||
17
src/game/replay_recorder.gd
Normal file
17
src/game/replay_recorder.gd
Normal file
@@ -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"
|
||||||
16
src/game/training_controller.gd
Normal file
16
src/game/training_controller.gd
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user