From 7963df5236cfafa3dba24803823e7f006946350b Mon Sep 17 00:00:00 2001 From: MDietermann Date: Wed, 12 Aug 2026 20:39:54 +0200 Subject: [PATCH] fix(kampf): geglueckte Flucht bricht die Initiativreihenfolge ab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Reihenfolge steht vor dem Zug fest, Flucht leert `enemies` aber mitten darin — danach zeigten die Indizes ins Leere und ein Gegner, der nach dem Helden drankam, stuerzte das Spiel ab. Zwei Zusagen aus Ticket 14, jede fuer sich richtig, nie zusammen geprueft: alle Kampftests loesten die Flucht am Ende der Reihenfolge aus. --- .../14-kampfformeln-und-rundenablauf.md | 19 ++++++++++ .scratch/rustkeep-1.0/map.md | 5 +++ src/game/mod.rs | 38 ++++++++++++++++++- 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/.scratch/rustkeep-1.0/issues/14-kampfformeln-und-rundenablauf.md b/.scratch/rustkeep-1.0/issues/14-kampfformeln-und-rundenablauf.md index a2a2988..b3ef225 100644 --- a/.scratch/rustkeep-1.0/issues/14-kampfformeln-und-rundenablauf.md +++ b/.scratch/rustkeep-1.0/issues/14-kampfformeln-und-rundenablauf.md @@ -240,3 +240,22 @@ zusätzlichen Zug auf jeder Beutetabelle. Damit hat `[balance]` acht Felder. - **15 Progression**: was bei Sieg passiert (EXP), und die Zahlen hinter allen Formeln inklusive der fünf `[balance]`-Knöpfe. - **16 Loot-Tabellen**: Beute hängt an der `creatures`-Registry, die hier entstanden ist. + +### Befund aus M4 — die Initiativreihenfolge ist ein Schnappschuss + +Der Zugablauf legt die Reihenfolge **einmal vor dem Zug** fest und arbeitet sie danach ab. Das war +richtig gedacht — sonst könnte ein Gegner sich durch Handeln in der Reihenfolge nach vorn schieben. +Nur passt es nicht zu einer Zusage aus demselben Ticket: **Flucht beendet den Kampf sofort**, leert +also `enemies` mitten in der Schleife. Die Reihenfolge zeigt danach auf Indizes, die es nicht mehr +gibt. + +Im Spiel sichtbar als Absturz nach mehrfachem Fluchtversuch — die Flucht muss glücken, *bevor* ein +Gegner in der Reihenfolge an der Reihe war, sonst fällt es nicht auf. Kein Test hatte den Fall, weil +alle Kampftests die Flucht am Ende der Reihenfolge auslösten. + +Entschieden: Ein leerer `enemies` **bricht den Zug ab**, statt still übersprungen zu werden. Kein +Gegner mehr da heißt, es gibt nichts mehr zu handeln — ein `continue` würde denselben Absturz nur +verschieben, wenn später jemand die Liste wieder füllt. + +Damit ist die Regel dieses Tickets vollständig: Die Reihenfolge steht vor dem Zug fest, *und* sie +endet, sobald ihre Grundlage weg ist. diff --git a/.scratch/rustkeep-1.0/map.md b/.scratch/rustkeep-1.0/map.md index 9cfabd2..c2038db 100644 --- a/.scratch/rustkeep-1.0/map.md +++ b/.scratch/rustkeep-1.0/map.md @@ -110,6 +110,11 @@ niemand hatte sie gebaut. Der Beutezieher schob für `count = "50-120"` bis zu * ins Inventar. Gefunden nicht beim Nachdenken, sondern beim Entwerfen des Handels: „Gold abziehen" braucht einen Stapel, sonst ist Bezahlen eine Schleife über Einträge. +Und **ein Absturz, den erst das Spielen gefunden hat**: Die Initiativreihenfolge ist ein Schnappschuss +vor dem Zug, eine geglückte Flucht leert `enemies` aber mitten darin — die Reihenfolge zeigt danach ins +Leere. Zwei Zusagen aus **14**, jede für sich richtig, die zusammen nicht geprüft worden waren. Ein +leerer `enemies` bricht den Zug jetzt ab. + Dazu **eine bewusst offene Stelle**: `Npc.triggers` ist nicht gebaut, weil `triggers_for` NPCs nicht einsammelt — ein Feld ohne Leser wäre eine Zusage, die niemand einlöst. TODO steht im Code. diff --git a/src/game/mod.rs b/src/game/mod.rs index 8c8cd18..019303f 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -503,7 +503,11 @@ impl Game { } match who { CreatureRef::Player => self.perform(&cmd, reg), - CreatureRef::Enemy(i) => self.enemy_acts(i, reg), + CreatureRef::Enemy(i) if i < self.enemies.len() => self.enemy_acts(i, reg), + // Eine geglueckte Flucht leert `enemies` mitten in der + // Reihenfolge — die stand vor dem Zug fest und zeigt danach ins + // Leere. Kein Gegner mehr da heisst: der Zug ist vorbei. + CreatureRef::Enemy(_) => break, } // 4. nach *jeder* Handlung leerarbeiten self.drain_events(reg); @@ -1559,4 +1563,36 @@ mod tests { assert_eq!(game.shop_stock().expect("Korb").len(), ware.len() - 1); assert!(game.player.inventory.iter().any(|i| i.def == ware[0].def)); } + + /// Gefunden im Spiel, nicht im Test: Wer oft genug flieht, trifft den Zug, + /// in dem die Flucht **vor** dem Handeln eines Gegners glueckt — dann zeigt + /// die vorher festgelegte Initiativreihenfolge auf einen geleerten `enemies`. + #[test] + fn geglueckte_flucht_bricht_die_initiativreihenfolge_ab() { + let (mut game, reg) = zwergenkrieger(); + + // Zwei Gegner, damit die Reihenfolge einen Index >0 enthaelt, und ein + // Held in der Mitte, damit nach seiner Flucht noch jemand drankaeme. + let ratte = reg.creatures.values().next().expect("mindestens ein Gegner"); + game.enemies = vec![Creature::from_def(ratte), Creature::from_def(ratte)]; + for (i, e) in game.enemies.iter_mut().enumerate() { + e.initiative = i as i32; + } + game.player.initiative = 1; + + // Flucht liegt bei 10-90 %, nach 30 Zuegen ist sie praktisch sicher. + for _ in 0..30 { + if game.enemies.is_empty() || game.over.is_some() { + break; + } + game.execute( + Command::UseAbility { + ability: String::from("core:flee"), + target: CreatureRef::Player, + }, + ®, + ); + } + assert!(game.enemies.is_empty(), "in 30 Zuegen muss eine Flucht geglueckt sein"); + } }