class_name BattleDebug extends Node #banc d'essai du moteur de combat (IA contre IA). Trois modes : # #1. COMBAT UNIQUE (par defaut) : format reglable dans l'inspecteur, journal complet. #2. SIMULATION DE MASSE : mass_simulation_count > 0 -> N combats aleatoires (formats, equipes et niveaux varies), # sans affichage. Le rapport liste les combats suspects (limite de tours atteinte, changements de Pokemon # excessifs...), chacun avec son "seed". #3. REJEU : replay_seed = un seed du rapport -> rejoue exactement ce combat, avec le journal complet. # #le journal n'est PAS ecrit dans la console (trop lent) : il est affiche a l'ecran et/ou ecrit dans un fichier. #la console ne recoit que de courtes lignes de progression. @export var run_on_ready : bool = true @export var max_turns : int = 100 #securite : match nul au-dela (0 = illimite) @export_group("Combat unique") @export_range(1, 3) var player_slots : int = 1 #emplacements du joueur @export_range(1, 5) var enemy_count : int = 1 #nombre d'adversaires (chacun est un participant) @export_range(1, 3) var enemy_slots_each : int = 1 #emplacements de chaque adversaire @export_range(1, 6) var team_size : int = 3 @export_range(1, 100) var level : int = 10 @export var use_game_team : bool = false #true : utilise GameData.player_team (qui sera modifiee !) @export_group("Simulation de masse") @export var mass_simulation_count : int = 0 #0 = desactivee @export_range(1, 100) var level_min : int = 5 @export_range(1, 100) var level_max : int = 50 @export var replay_seed : int = 0 #0 = desactive ; sinon rejoue le combat de ce seed @export_group("Sortie") @export var show_log_on_screen : bool = true @export var write_log_file : bool = true const LOG_PATH : String = "user://battle_debug.log" var _log_layer : CanvasLayer = null func _ready() -> void: if !run_on_ready: return await get_tree().create_timer(0.5).timeout if mass_simulation_count > 0: run_mass_simulation() elif replay_seed != 0: replay(replay_seed) else: run_test() #---------- 1. combat unique ---------- func run_test() -> void: var player_team : PokemonTeam = GameData.player_team if use_game_team else _random_team(maxi(team_size, player_slots)) var setup := BattleSetup.new() setup.add(BattleSetup.player_participant(player_team, player_slots, null, BattleAI.new())) for i in enemy_count: var team : PokemonTeam = _random_team(enemy_slots_each + 1) setup.add(BattleParticipant.new("Adversaire %d" % (i + 1), team, 1, BattleAI.new(), enemy_slots_each)) if !setup.is_valid(): push_error("BattleDebug : combat invalide (une equipe n'a aucun pokemon apte)") return await _run_with_log(setup, "Combat : %s" % _describe(setup)) func _random_team(size : int) -> PokemonTeam: var team := PokemonTeam.new() var ids : Array = Pokedex.all_ids() if ids.is_empty(): push_error("BattleDebug : le Pokedex ne contient aucun pokemon") return team for i in size: var species : PokemonData = Pokedex.get_pokemon(ids.pick_random()) if species: team.add_pokemon(PokemonInstance.create(species, level)) return team #---------- 3. rejeu ---------- func replay(seed_value : int) -> void: var setup : BattleSetup = _build_setup(seed_value) if !setup.is_valid(): push_error("BattleDebug : le seed %d ne donne pas de combat valide" % seed_value) return await _run_with_log(setup, "Rejeu du seed %d : %s" % [seed_value, _describe(setup)]) func _run_with_log(setup : BattleSetup, title : String) -> void: var presenter := BattleLogPresenter.new() var battle := Battle.new(setup, presenter) battle.max_turns = max_turns var outcome : Battle.Outcome = await battle.run() var summary : String = "Résultat : %s après %d tours (%d lignes de journal)" % [Battle.Outcome.keys()[outcome], battle.turn, presenter.lines.size()] var text : String = "===== " + title + " =====\n" + "\n".join(presenter.lines) + "\n===== " + summary + " =====" print("[BattleDebug] " + summary) _output(text, summary) #---------- 2. simulation de masse ---------- func run_mass_simulation() -> void: var base_seed : int = randi() % 900000 + 1 var wins : int = 0 var losses : int = 0 var draws : int = 0 var played : int = 0 var total_turns : int = 0 var max_turns_seen : int = 0 var total_switches : int = 0 var max_switches : int = 0 var suspects : PackedStringArray = PackedStringArray() for i in mass_simulation_count: var seed_value : int = base_seed + i var setup : BattleSetup = _build_setup(seed_value) if !setup.is_valid(): continue var stats := BattleStatsPresenter.new() var battle := Battle.new(setup, stats) battle.max_turns = max_turns var outcome : Battle.Outcome = await battle.run() played += 1 total_turns += battle.turn max_turns_seen = maxi(max_turns_seen, battle.turn) var switches : int = stats.count(BattleEvent.Type.WITHDRAWN) total_switches += switches max_switches = maxi(max_switches, switches) match outcome: Battle.Outcome.PLAYER_WON: wins += 1 Battle.Outcome.PLAYER_LOST: losses += 1 _: draws += 1 if outcome == Battle.Outcome.DRAW: suspects.append("seed %d | %s | match nul : %d tours sans vainqueur" % [seed_value, _describe(setup), battle.turn]) elif switches > battle.turn: suspects.append("seed %d | %s | %d changements de Pokémon en %d tours" % [seed_value, _describe(setup), switches, battle.turn]) #le moteur tourne sans rendre la main : on laisse respirer l'editeur if i % 10 == 9: print("[BattleDebug] simulation %d/%d" % [i + 1, mass_simulation_count]) await get_tree().process_frame var n : float = maxf(played, 1) var lines : PackedStringArray = PackedStringArray() lines.append("===== Simulation de masse : %d combats =====" % played) lines.append("Victoires du camp 1 : %d | Défaites : %d | Nuls (limite de %d tours) : %d" % [wins, losses, max_turns, draws]) lines.append("Tours : moyenne %.1f, maximum %d" % [total_turns / n, max_turns_seen]) lines.append("Changements de Pokémon par combat : moyenne %.1f, maximum %d" % [total_switches / n, max_switches]) lines.append("") if suspects.is_empty(): lines.append("Aucun combat suspect.") else: lines.append("%d combat(s) suspect(s) :" % suspects.size()) lines.append_array(suspects.slice(0, 50)) lines.append("") lines.append("Pour en rejouer un : replay_seed = , mass_simulation_count = 0, puis relance.") var summary : String = "%d combats, %d suspect(s)" % [played, suspects.size()] print("[BattleDebug] " + summary) _output("\n".join(lines), summary) #---------- construction reproductible ---------- func _build_setup(seed_value : int) -> BattleSetup: #tout (format, equipes, niveaux, natures, combat) est derive du seed : meme seed = meme combat var rng := RandomNumberGenerator.new() rng.seed = seed_value var ids : Array = Pokedex.all_ids() var setup := BattleSetup.new() setup.seed_value = seed_value setup.can_flee = false if ids.is_empty(): push_error("BattleDebug : le Pokedex ne contient aucun pokemon") return setup var my_slots : int = rng.randi_range(1, 3) setup.add(BattleSetup.player_participant(_seeded_team(rng, ids, rng.randi_range(my_slots, 6)), my_slots, null, BattleAI.new())) var enemy_participants : int = rng.randi_range(1, 3) for i in enemy_participants: var slots : int = rng.randi_range(1, 3) var team : PokemonTeam = _seeded_team(rng, ids, rng.randi_range(slots, 6)) setup.add(BattleParticipant.new("Adversaire %d" % (i + 1), team, 1, BattleAI.new(), slots)) return setup func _seeded_team(rng : RandomNumberGenerator, ids : Array, size : int) -> PokemonTeam: var team := PokemonTeam.new() for i in size: var species : PokemonData = Pokedex.get_pokemon(ids[rng.randi_range(0, ids.size() - 1)]) if species == null: continue var ability : AbilityData = PokemonInstance.pick_default_ability(species, rng) team.add_pokemon(PokemonInstance.create(species, rng.randi_range(level_min, level_max), rng.randi_range(0, 24), null, ability)) return team func _describe(setup : BattleSetup) -> String: var mine : int = 0 var enemies : PackedStringArray = PackedStringArray() for p in setup.participants: if p.side == 0: mine += p.slot_count else: enemies.append(str(p.slot_count)) return "%d contre %s" % [mine, "+".join(enemies)] #---------- sortie ---------- func _output(text : String, summary : String) -> void: if write_log_file: _write_log(text) if show_log_on_screen: _show_log(text, summary) func _write_log(text : String) -> void: var file := FileAccess.open(LOG_PATH, FileAccess.WRITE) if file == null: push_warning("BattleDebug : impossible d'ecrire le journal (%s)" % LOG_PATH) return file.store_string(text) file.close() print("[BattleDebug] Journal ecrit dans : " + ProjectSettings.globalize_path(LOG_PATH)) func _show_log(text : String, summary : String) -> void: #panneau plein ecran avec le journal defilant, ferme par un bouton if _log_layer != null: _log_layer.queue_free() _log_layer = CanvasLayer.new() _log_layer.layer = 100 add_child(_log_layer) var panel := PanelContainer.new() panel.set_anchors_preset(Control.PRESET_FULL_RECT) _log_layer.add_child(panel) var box := VBoxContainer.new() panel.add_child(box) var title := Label.new() title.text = summary box.add_child(title) var log_label := RichTextLabel.new() log_label.size_flags_vertical = Control.SIZE_EXPAND_FILL log_label.selection_enabled = true log_label.scroll_active = true log_label.text = text box.add_child(log_label) var close := Button.new() close.text = "Fermer" close.pressed.connect(func() -> void: _log_layer.queue_free() _log_layer = null) box.add_child(close) close.grab_focus()