From 7259616b8b73dd99b11a734970769e211f3a5c9c Mon Sep 17 00:00:00 2001 From: TheThomaas Date: Sun, 18 Oct 2026 11:32:51 +0200 Subject: [PATCH] Update system --- game/battle/battle.gd | 4 + game/battle/battle_debug.gd | 186 ++++++++++++++++++---- game/battle/battle_stats_presenter.gd | 12 ++ game/battle/battle_stats_presenter.gd.uid | 1 + game/battle/target_resolver.gd | 6 +- game/world/battle.tscn | 1 + 6 files changed, 179 insertions(+), 31 deletions(-) create mode 100644 game/battle/battle_stats_presenter.gd create mode 100644 game/battle/battle_stats_presenter.gd.uid diff --git a/game/battle/battle.gd b/game/battle/battle.gd index 68f6fb0..ed4b95f 100644 --- a/game/battle/battle.gd +++ b/game/battle/battle.gd @@ -245,6 +245,10 @@ func _replace_fainted() -> void: #---------- requetes (utilisees par les controleurs et TargetResolver) ---------- +func random_pick(items : Array) -> Variant: + #tirage dans le generateur du combat (et non le hasard global) : un meme seed rejoue exactement le meme combat + return items[rng.randi_range(0, items.size() - 1)] + func get_slot(id : int) -> BattleSlot: return slots[id] if id >= 0 and id < slots.size() else null diff --git a/game/battle/battle_debug.gd b/game/battle/battle_debug.gd index 1e4000d..3734089 100644 --- a/game/battle/battle_debug.gd +++ b/game/battle/battle_debug.gd @@ -1,20 +1,33 @@ class_name BattleDebug extends Node -#banc d'essai : simule un combat complet IA contre IA. -#le journal n'est PAS ecrit dans la console (trop lent) : il est garde en memoire, puis -#affiche a l'ecran et/ou ecrit dans un fichier a la fin du combat. La console ne recoit qu'un resume. -#ajoute ce node a une scene, regle le format dans l'inspecteur, lance la scene. -#utilise des equipes aleatoires (par defaut) : n'abime pas ton equipe de jeu. +#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 var max_turns : int = 100 #securite : match nul au-dela (0 = illimite) + +@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 @@ -24,39 +37,27 @@ const LOG_PATH : String = "user://battle_debug.log" var _log_layer : CanvasLayer = null func _ready() -> void: - if run_on_ready: - await get_tree().create_timer(0.5).timeout - run_test() + 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: - if use_game_team: - GameData.load_data() - 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) - var participant := BattleParticipant.new("Adversaire %d" % (i + 1), team, 1, BattleAI.new(), enemy_slots_each) - setup.add(participant) + 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 - - var presenter: BattleLogPresenter = BattleLogPresenter.new() - var battle := Battle.new(setup, presenter) - battle.max_turns = max_turns - var outcome : Battle.Outcome = await battle.run() - - var header : String = "===== Combat : %d contre %d =====" % [player_slots, enemy_count * enemy_slots_each] - 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 = header + "\n" + "\n".join(presenter.lines) + "\n===== " + summary + " =====" - - print("[BattleDebug] " + summary) #une seule ligne dans la console - if write_log_file: _write_log(text) - if show_log_on_screen: _show_log(text, summary) + await _run_with_log(setup, "Combat : %s" % _describe(setup)) func _random_team(size : int) -> PokemonTeam: var team := PokemonTeam.new() @@ -69,8 +70,137 @@ func _random_team(size : int) -> PokemonTeam: 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 + team.add_pokemon(PokemonInstance.create(species, rng.randi_range(level_min, level_max), rng.randi_range(0, 24))) + 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: @@ -81,7 +211,7 @@ func _write_log(text : String) -> void: 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 (ou B / Echap) + #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 diff --git a/game/battle/battle_stats_presenter.gd b/game/battle/battle_stats_presenter.gd new file mode 100644 index 0000000..13e8202 --- /dev/null +++ b/game/battle/battle_stats_presenter.gd @@ -0,0 +1,12 @@ +class_name BattleStatsPresenter +extends BattlePresenter + +#presentation "muette" pour la simulation de masse : ne garde que le nombre d'evenements par type + +var counts : Dictionary = {} + +func present(event : BattleEvent) -> void: + counts[event.type] = int(counts.get(event.type, 0)) + 1 + +func count(type : BattleEvent.Type) -> int: + return int(counts.get(type, 0)) diff --git a/game/battle/battle_stats_presenter.gd.uid b/game/battle/battle_stats_presenter.gd.uid new file mode 100644 index 0000000..e21cf9f --- /dev/null +++ b/game/battle/battle_stats_presenter.gd.uid @@ -0,0 +1 @@ +uid://cq1cg5ffpmhgy diff --git a/game/battle/target_resolver.gd b/game/battle/target_resolver.gd index 99a5289..200b919 100644 --- a/game/battle/target_resolver.gd +++ b/game/battle/target_resolver.gd @@ -38,7 +38,7 @@ static func resolve(battle : Battle, actor : BattleSlot, move : MoveData, chosen "all-opponents": result = opponents "random-opponent": - if !opponents.is_empty(): result.append(opponents.pick_random()) + if !opponents.is_empty(): result.append(battle.random_pick(opponents)) "all-other-pokemon": result.append_array(opponents) result.append_array(allies) @@ -53,7 +53,7 @@ static func resolve(battle : Battle, actor : BattleSlot, move : MoveData, chosen if chosen != null and chosen.is_active() and (allies.has(chosen) or chosen == actor): result.append(chosen) elif !allies.is_empty(): - result.append(allies.pick_random()) + result.append(battle.random_pick(allies)) elif get_target_type(move) == "user-or-ally": result.append(actor) "users-field", "opponents-field", "fainting-pokemon": @@ -63,5 +63,5 @@ static func resolve(battle : Battle, actor : BattleSlot, move : MoveData, chosen if chosen != null and chosen != actor and chosen.is_active(): result.append(chosen) elif !opponents.is_empty(): - result.append(opponents.pick_random()) + result.append(battle.random_pick(opponents)) return result diff --git a/game/world/battle.tscn b/game/world/battle.tscn index 264c155..3266bea 100644 --- a/game/world/battle.tscn +++ b/game/world/battle.tscn @@ -4,3 +4,4 @@ [node name="Battle" type="Node" unique_id=520840168] script = ExtResource("1_5bjgp") +replay_seed = 881794