fix(kampf): geglueckte Flucht bricht die Initiativreihenfolge ab

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.
This commit is contained in:
2026-08-12 20:39:54 +02:00
parent 3a6af6affb
commit 7963df5236
3 changed files with 61 additions and 1 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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,
},
&reg,
);
}
assert!(game.enemies.is_empty(), "in 30 Zuegen muss eine Flucht geglueckt sein");
}
}