Flotten und die Weltraumschlacht
Kurz gesagt: Eine Flotte ist ein Array mit der Zahl der Schiffe je Rolle. Die Rollen sind Schere-Stein-Papier (ein Multiplikator in Prozent), jede Gruppe schießt auf das günstigste Ziel, Schaden, der nicht für ein ganzes Schiff reicht, bleibt als Wunde, und die beiden Salven einer Runde werden aus den Zahlen zu Rundenbeginn berechnet und treffen gleichzeitig. Bei den Zügen ist es genauso: Erst brechen alle auf, dann kommen alle an.
Problem
Wer schießt zuerst?
Zwei Flotten treffen in einem System aufeinander. Die erste Antwort lautet: der Reihe nach, erst schießt die erste Seite, dann die zweite. Aber dann gewinnt die erste Seite nur wegen der Reihenfolge im Code: Die Schiffe, die sie zerstört hat, antworten nicht mehr. Das Spiel will etwas anderes: In einer Runde schießen beide Seiten gleichzeitig, und auch die Flotten bewegen sich alle auf einmal zwischen den Systemen.
Hier ist ein Bild aus dem Spiel (der Befehl battle, aus dem Durchlauf, an dem das Spiel geprüft wird): bei Ismar vier Abfangjäger gegen zwei Bomber und zwei Fregatten. Die leeren Zeilen des Bildschirms sind gekürzt.
Runde 28/150 Terranisches Reich (A) Punkte 147M3+27 G203+36 C11+24 U52+5 Forschung +16Forscht: Torpedos 28/31Kampf bei Ismar: Terranisches Reich gegen Orion-HegemonieRunde 1: 4I | 2B 2F Verluste 2 / 2Runde 2: 2I | 2F Verluste 2 / 0Sieg für Orion-Hegemonie. Übrig: 2F.
Enter oder map: zur Karte help: BefehleDas Spiel faltet die Leerzeichen in den Zeilen (battle bricht sie auf die Breite des Bildschirms um), darum hat „Runde 1: 4I | 2B 2F“ keine Spalten. Die Programme der Lektion drucken dieselben Wörter.
Für dieses Bild braucht es fünf Dinge: eine Flotte (Schiffszahl je Rolle), Rollen (Schere-Stein-Papier), eine Salve (jede Gruppe schießt auf das günstigste Ziel), eine Wunde (Schaden, der nicht für ein ganzes Schiff reicht, geht nicht verloren) und Gleichzeitigkeit (von Salven und von Zügen). Die Lektion baut sie eins nach dem anderen mit kleinen Programmen und fügt sie am Ende zu derselben Schlacht zusammen. In der Lektion über das Kampfsystem fallen die Schläge einzeln und mit einem Würfel. Hier gibt es keinen Würfel: Die Schlacht ist eine reine Funktion, dieselben zwei Flotten ergeben dasselbe Ergebnis.
Die Idee
Eine Flotte ist eine Zahl
Eine Flotte ist keine Liste von Schiffen, sondern fünf Zahlen: wie viele Abfangjäger (I), Bomber (B), Fregatten (F), Schlachtschiffe (D) und Festungen (#) ein Imperium in einem System hat. Alle Schiffe einer Rolle sind gleich, darum braucht es kein Objekt pro Schiff: Eine Schlacht mit 500 Schiffen sind dieselben fünf Zahlen. Die Festungen der Planeten dort kämpfen als fünfte Rolle.
Die Werte der Rollen sind zwei Tabellen, die nach Rolle indiziert sind: Hülle und Angriff. Die dritte Tabelle ist der Multiplikator, um den es im nächsten Schritt geht:
const KINDS = 5;const ROUNDS = 6;const LETTERS = "IBFD#";const HP = [10, 16, 30, 90, 25];const ATK = [5, 7, 6, 18, 5];const MULT = [ [100, 175, 50, 50, 60], [50, 100, 175, 80, 150], [175, 50, 100, 80, 100], [110, 110, 110, 100, 110], [100, 100, 100, 100, 100],];Zwei kleine Funktionen braucht man überall: die Summe der Schiffe und den Text „3I 2B 2F“, den das Spiel für eine Flotte schreibt (eine leere ist „-“). Die Macht einer Rolle ist Hülle × Angriff / 10, die einer Flotte die Summe über die Rollen. Das Spiel nutzt sie, um Flotten zu vergleichen (die künstliche Intelligenz, die nächsten Lektionen).
function total(counts) { let sum = 0; for (const n of counts) sum += n; return sum;}
function countsText(counts) { const parts = []; for (let k = 0; k < KINDS; k++) { if (counts[k] > 0) parts.push(counts[k] + LETTERS[k]); } return parts.length === 0 ? "-" : parts.join(" ");}function unitPower(kind) { return Math.floor((HP[kind] * ATK[kind]) / 10);}
function power(counts) { let sum = 0; for (let k = 0; k < KINDS; k++) sum += counts[k] * unitPower(k); return sum;}Das Programm zeigt die Flotte 3I 2B 2F: Text, Anzahl, Macht und eine leere Flotte.
const fleet = [3, 2, 2, 0, 0];console.log("Flotte: " + countsText(fleet));console.log("Schiffe: " + total(fleet));console.log("Macht: " + power(fleet));console.log("Leere Flotte: " + countsText(new Array(KINDS).fill(0)));Flotte: 3I 2B 2FSchiffe: 7Macht: 73Leere Flotte: -Die Macht ist 3 × 5 + 2 × 11 + 2 × 18 = 73. Der Abfangjäger hat 5, der Bomber 11, die Fregatte 18 und das Schlachtschiff 162.
Math.floor(a / b) ist die Ganzzahldivision: In JavaScript ist / immer eine Division mit Nachkommastellen.Im Spiel liegen die Flotten in einer Liste world.fleets, geordnet nach Imperium und dann nach System. fleetAt sucht die Flotte eines Imperiums in einem System, und addShips legt sie an (an ihrer Stelle in der Reihenfolge) oder fügt der vorhandenen Schiffe hinzu:
function fleetAt(world, owner, systemId) { for (const fleet of world.fleets) { if (fleet.owner === owner && fleet.systemId === systemId) return fleet; } return null;}// Fügt der Flotte des Reiches im System Schiffe hinzu; eine neue Flotte kommt an ihren Platz in der Reihenfolge.function addShips(world, owner, systemId, counts) { let fleet = fleetAt(world, owner, systemId); if (fleet === null) { fleet = { owner: owner, systemId: systemId, ships: [0, 0, 0, 0] }; let at = world.fleets.length; while (at > 0) { const before = world.fleets[at - 1]; if (before.owner < owner || (before.owner === owner && before.systemId < systemId)) break; at--; } world.fleets.splice(at, 0, fleet); } for (let k = 0; k < SHIP_KIND_COUNT; k++) fleet.ships[k] += counts[k];}Die Idee
Schere, Stein, Papier
Wenn nur die Macht zählte, würde es sich immer lohnen, das stärkste Schiff zu bauen. Darum kämpfen die Rollen unterschiedlich gegeneinander. Der Multiplikator ist ein Prozentsatz des Schadens: Die Zeile ist der Angreifer, die Spalte das Ziel. Der Abfangjäger schlägt den Bomber (175%), der Bomber schlägt die Fregatte (175%), die Fregatte schlägt den Abfangjäger (175%), gegen die Rolle, die ihn schlägt, trifft jeder nur zur Hälfte (50%), und gegen die eigene Rolle mit 100%. Das Schlachtschiff trifft alle mit 100–110%, und es selbst wird von allen schwach getroffen (50–80%): Es ist aus sich heraus stark, nicht durch seine Rolle.
// Der Schadensfaktor in Prozent: die Zeile ist der Angreifer, die Spalte das Ziel (Abfangjäger, Bomber,// Fregatte, Schlachtschiff, Festung).const MULT = [ [100, 175, 50, 50, 60], [50, 100, 175, 80, 150], [175, 50, 100, 80, 100], [110, 110, 110, 100, 110], [100, 100, 100, 100, 100],];Jede Gruppe (alle Schiffe einer Rolle) schießt auf ein Ziel: die Rolle unter den lebenden Feinden, gegen die der Multiplikator am größten ist. Bei Gleichstand gewinnt die erste in der Reihenfolge: Der Vergleich ist strikt „größer“, darum ersetzt eine spätere Rolle mit gleichem Multiplikator die bereits gewählte nicht.
function chooseTarget(kind, enemy) { let best = -1; for (let k = 0; k < KINDS; k++) { if (enemy[k] > 0 && (best < 0 || MULT[kind][k] > MULT[kind][best])) best = k; } return best;}function chooseTarget(kind, enemy) {
let best = -1;
for (let k = 0; k < KINDS; k++) {
if (enemy[k] > 0 && (best < 0 || MULT[kind][k] MULT[kind][best])) best = k;
}
return best;
}Der Vergleich ist „größer“: >. Mit >= würde bei Gleichstand die letzte Rolle gewinnen: Das Programm liefe trotzdem, aber die Schlachten wären andere. Mit > hängt das Ziel nur von der Reihenfolge der Rollen ab, darum ist das Ergebnis in allen vier Sprachen dasselbe.
Das Programm zeigt die Ziele der fünf Rollen gegen eine volle Flotte und einen Fall, in dem die günstigste Rolle fehlt:
const all = [1, 1, 1, 1, 1];for (let kind = 0; kind < KINDS; kind++) { const target = chooseTarget(kind, all); console.log(NAMES[kind] + " -> " + NAMES[target] + " " + MULT[kind][target] + "%");}const noFrigates = [1, 1, 0, 1, 1];const alt = chooseTarget(1, noFrigates);console.log("Ohne Fregatten: " + NAMES[1] + " -> " + NAMES[alt] + " " + MULT[1][alt] + "%");Abfangjäger -> Bomber 175%Bomber -> Fregatte 175%Fregatte -> Abfangjäger 175%Schlachtschiff -> Abfangjäger 110%Festung -> Abfangjäger 100%Ohne Fregatten: Bomber -> Festung 150%Die ersten drei Zeilen sind das Dreieck. Das Schlachtschiff und die Festung haben keine Rolle, gegen die sie stärker sind: Ihr größter Multiplikator (110% und 100%) steht an erster Stelle der Reihenfolge, beim Abfangjäger. Die letzte Zeile ist die interessanteste: Der Bomber will Fregatten, und es gibt keine, also wählt er das nächstbeste Ziel, die Festung (150%), nicht den Abfangjäger (50%) und nicht das Schlachtschiff (80%).
Hier ist die Funktion im Spiel:
function chooseTarget(kind, enemy) { let best = -1; for (let k = 0; k < KIND_COUNT; k++) { if (enemy[k] > 0 && (best < 0 || MULT[kind][k] > MULT[kind][best])) best = k; } return best;}Die Idee
Salve und Wunde
Die Gruppe, die ein Ziel gewählt hat, richtet Schaden an = Anzahl × Angriff × Multiplikator / 100. Die Division ist eine Ganzzahldivision und schneidet ab: 6 Bomber mit je 7 gegen Fregatten machen 6 × 7 × 175 / 100 = 73 (nicht 73,5). Der Schaden aller Gruppen einer Seite wird nach der Rolle des Ziels addiert.
function volley(mine, enemy) { const damage = new Array(KINDS).fill(0); for (let k = 0; k < KINDS; k++) { if (mine[k] === 0) continue; const target = chooseTarget(k, enemy); if (target < 0) continue; damage[target] += Math.floor((mine[k] * ATK[k] * MULT[k][target]) / 100); } return damage;}Dann wird der Schaden angewendet. Der Schaden auf eine Rolle wird durch die Hülle eines Schiffs geteilt: So viele ganze Schiffe, wie hineinpassen, fallen. Fallen alle, verschwindet die Rolle. Sonst wird der Rest (Schaden, der für ein weiteres Schiff nicht reicht) als Wunde gespeichert und zur nächsten Salve addiert. Ohne Wunden würde langsames Feuer nie ein Schiff töten: 9 Schaden auf ein Schiff mit Hülle 10 sind nichts, aber beim zweiten Mal sind es 18, und ein Schiff fällt.
function applyDamage(counts, wound, damage) { for (let k = 0; k < KINDS; k++) { if (damage[k] === 0 || counts[k] === 0) continue; const sum = wound[k] + damage[k]; const killed = Math.floor(sum / HP[k]); if (killed >= counts[k]) { counts[k] = 0; wound[k] = 0; } else { counts[k] -= killed; wound[k] = sum - killed * HP[k]; } }}Das Programm macht die erste Runde der Schlacht bei Ismar: die beiden Salven (die Spalten sind I, B, F, D, #), dann das Anwenden. Die kleine Funktion numbers druckt ein Array mit Leerzeichen:
function numbers(values) { return values.join(" ");}Das Programm:
const a = [4, 0, 0, 0, 0];const b = [0, 2, 2, 0, 0];const hitB = volley(a, b);const hitA = volley(b, a);console.log("a -> b: " + numbers(hitB));console.log("b -> a: " + numbers(hitA));const woundA = new Array(KINDS).fill(0);const woundB = new Array(KINDS).fill(0);applyDamage(b, woundB, hitB);applyDamage(a, woundA, hitA);console.log("a: " + countsText(a) + ", Wunden " + numbers(woundA));console.log("b: " + countsText(b) + ", Wunden " + numbers(woundB));a -> b: 0 35 0 0 0b -> a: 28 0 0 0 0a: 2I, Wunden 8 0 0 0 0b: 2F, Wunden 0 0 0 0 0Die Seite a trifft nur die Bomber: 4 × 5 × 175 / 100 = 35, und zwei Bomber haben 32 Hülle, also fallen beide (35 / 16 = 2). Die Seite b trifft nur die Abfangjäger: die Bomber mit 50% (2 × 7 × 50 / 100 = 7), die Fregatten mit 175% (2 × 6 × 175 / 100 = 21), zusammen 28. 28 / 10 = 2 Abfangjäger fallen, und 8 bleiben als Wunde am dritten. Die Fregatten werden in der ersten Runde nicht getroffen: Die Abfangjäger schießen nur auf die Bomber.
Hier sind die beiden Funktionen im Spiel. Außer den Schiffszahlen und dem Schaden bekommen sie stats (die Boni aus Technologien); die gibt es hier nicht, darum sind die Zahlen des Programms die eines Imperiums ohne Technologien. In Java ist der Schaden ein long, weil die Schiffszahl aus einem Spielstand groß sein kann und ein int überlaufen würde:
// Der Schaden, den eine Seite der anderen zufügt, nach Rolle des Ziels.function volley(stats, mine, enemy) { const damage = emptyCounts(); for (let k = 0; k < KIND_COUNT; k++) { if (mine[k] === 0) continue; const target = chooseTarget(k, enemy); if (target < 0) continue; damage[target] += Math.floor((mine[k] * stats.atk[k] * MULT[k][target]) / 100); } return damage;}// Der Schaden summiert sich: die Einheiten verlieren Hülle, aber der erste Überlebende behält seine Wunde für die nächste Runde.function applyDamage(stats, counts, wound, damage) { for (let k = 0; k < KIND_COUNT; k++) { if (damage[k] === 0 || counts[k] === 0) continue; const total = wound[k] + damage[k]; const killed = Math.floor(total / stats.hp[k]); if (killed >= counts[k]) { counts[k] = 0; wound[k] = 0; } else { counts[k] -= killed; wound[k] = total - killed * stats.hp[k]; } }}Sieh es dir an
Die ganze Runde auf einmal
Jetzt kommt alles zusammen. Die Schleife der Schlacht macht in jeder Runde Folgendes:
- Hat eine Seite keine Schiffe, ist Schluss.
- Sie kopiert die Zahlen beider Seiten: Das ist der „Schnappschuss“ vom Rundenbeginn.
- Sie berechnet beide Salven aus den Schnappschüssen: die Salve von a auf b und die Salve von b auf a.
- Sie wendet den Schaden auf b an, dann auf a. Die Reihenfolge spielt keine Rolle, denn die Salven sind schon berechnet.
- Sie schreibt die Zeile der Runde: Wie viele Schiffe beide Seiten zu Beginn hatten und wie viele sie verloren haben.
Nach sechs Runden gibt es keinen Sieger, wenn noch beide Seiten Schiffe haben. Hier ist die ganze Funktion und das Programm, das die Schlacht bei Ismar schlägt:
function runBattle(a, b, nameA, nameB, log) { const woundA = new Array(KINDS).fill(0); const woundB = new Array(KINDS).fill(0); for (let round = 1; round <= ROUNDS; round++) { if (total(a) === 0 || total(b) === 0) break; const startA = [...a]; const startB = [...b]; const hitB = volley(startA, startB); const hitA = volley(startB, startA); applyDamage(b, woundB, hitB); applyDamage(a, woundA, hitA); log.push("Runde " + round + ": " + countsText(startA) + " | " + countsText(startB) + " Verluste " + (total(startA) - total(a)) + " / " + (total(startB) - total(b))); } if (total(a) > 0 && total(b) === 0) log.push("Sieg für " + nameA + ". Übrig: " + countsText(a) + "."); else if (total(b) > 0 && total(a) === 0) log.push("Sieg für " + nameB + ". Übrig: " + countsText(b) + "."); else if (total(a) === 0 && total(b) === 0) log.push("Beide Flotten sind vernichtet."); else log.push("Kein Sieger nach sechs Runden: " + countsText(a) + " | " + countsText(b) + ".");}const a = [4, 0, 0, 0, 0];const b = [0, 2, 2, 0, 0];const where = "Ismar";const nameA = "Terranisches Reich";const nameB = "Orion-Hegemonie";const log = [];runBattle(a, b, nameA, nameB, log);console.log("Kampf bei " + where + ": " + nameA + " gegen " + nameB);for (const line of log) console.log(line);Kampf bei Ismar: Terranisches Reich gegen Orion-HegemonieRunde 1: 4I | 2B 2F Verluste 2 / 2Runde 2: 2I | 2F Verluste 2 / 0Sieg für Orion-Hegemonie. Übrig: 2F.Das sind genau die Zeilen des Spiels. Die zweite Runde ist kurz: Von den vier Abfangjägern sind zwei übrig (mit einer Wunde von 8), und von den Bombern gibt es keine Spur. Die zwei Abfangjäger treffen die Fregatten nur mit 50% (2 × 5 × 50 / 100 = 5 Schaden, weniger als die Hülle eines Schiffs), und die Fregatten treffen die Abfangjäger mit 175%: 2 × 6 × 175 / 100 = 21, plus die Wunde 8 ergibt 29. Beide Abfangjäger fallen. Die Fregatten gewinnen, ohne ein Schiff zu verlieren. Verfolge die Runden in der Szene: Die Pfeile und die Zahlen sind gleichzeitig.
function runBattle(a, b, nameA, nameB, log) { const woundA = new Array(KINDS).fill(0); const woundB = new Array(KINDS).fill(0); for (let round = 1; round <= ROUNDS; round++) { if (total(a) === 0 || total(b) === 0) break; const startA = [...a]; const startB = [...b]; const hitB = volley(startA, startB); const hitA = volley(startB, startA); applyDamage(b, woundB, hitB); applyDamage(a, woundA, hitA); log.push("Runde " + round + ": " + countsText(startA) + " | " + countsText(startB) + " Verluste " + (total(startA) - total(a)) + " / " + (total(startB) - total(b))); } if (total(a) > 0 && total(b) === 0) log.push("Sieg für " + nameA + ". Übrig: " + countsText(a) + "."); else if (total(b) > 0 && total(a) === 0) log.push("Sieg für " + nameB + ". Übrig: " + countsText(b) + "."); else if (total(a) === 0 && total(b) === 0) log.push("Beide Flotten sind vernichtet."); else log.push("Kein Sieger nach sechs Runden: " + countsText(a) + " | " + countsText(b) + ".");}Schritt für Schritt, in Worten
- Zwei Flotten bei Ismar: a hat 4I, b hat 2B 2F. Eine Flotte ist nur die Zahl der Schiffe je Rolle, der Kampf eine Funktion zweier solcher Arrays.
- Runde 1. Jede Seite kopiert ihre Zahlen zu Rundenbeginn. Beide Salven werden aus diesen Kopien berechnet.
- Jede Rolle wählt das Ziel mit dem größten Multiplikator. a: 4I → B (175%). b: 2B → I (50%), 2F → I (175%). Schere-Stein-Papier.
- Die Salven: a: 4 × 5 × 175 / 100 = 35 auf die Bomber. b: 2 × 7 × 50 / 100 = 7; 2 × 6 × 175 / 100 = 21 auf die Abfangjäger, zusammen 28. Sie treffen gleichzeitig.
- Der Schaden wird angewendet: 35 / 16 = 2 Bomber fallen. 28 / 10 = 2 Abfangjäger fallen, der Rest 8 ist eine Wunde. Das Protokoll: Verluste 2 / 2.
- Runde 2: 2I gegen 2F. Die Bomber sind weg, also werden die Ziele neu gewählt.
- Die Abfangjäger treffen die Fregatten nur mit 50 %: 2 × 5 × 50 / 100 = 5. Die Fregatten treffen die Abfangjäger mit 175 %: 2 × 6 × 175 / 100 = 21.
- Der Schaden auf die Abfangjäger ist 21 plus die Wunde 8 = 29: Beide fallen. Die Fregatten bekommen 5, weniger als Hülle 30: Wunde 5, keine fällt.
- a hat keine Schiffe, b hat 2F: Sieg für Orion-Hegemonie. Übrig: 2F. So ist es auch im Spiel. Die Schlacht ist eine reine Funktion.
Im Spiel ist die Schlacht dieselbe Funktion, nur mit mehr Parametern (stats für Technologien), und sie gibt eine Liste von Runden zurück, während eine andere Funktion den Text schreibt. Hier ist sie:
function runBattle(statsA, countsA, statsB, countsB) { const a = countsA.slice(); const b = countsB.slice(); const woundA = emptyCounts(); const woundB = emptyCounts(); const rounds = []; for (let round = 1; round <= BATTLE_ROUNDS; round++) { if (totalOf(a) === 0 || totalOf(b) === 0) break; const startA = a.slice(); const startB = b.slice(); const hitB = volley(statsA, startA, startB); const hitA = volley(statsB, startB, startA); applyDamage(statsB, b, woundB, hitB); applyDamage(statsA, a, woundA, hitA); const lostA = emptyCounts(); const lostB = emptyCounts(); for (let k = 0; k < KIND_COUNT; k++) { lostA[k] = startA[k] - a[k]; lostB[k] = startB[k] - b[k]; } rounds.push({ a: startA, b: startB, lostA: lostA, lostB: lostB }); } let winner = 'draw'; if (totalOf(a) > 0 && totalOf(b) === 0) winner = 'a'; else if (totalOf(b) > 0 && totalOf(a) === 0) winner = 'b'; else if (totalOf(a) === 0 && totalOf(b) === 0) winner = 'none'; return { rounds: rounds, a: a, b: b, winner: winner };}Genauer, für Neugierige
Die Schlacht ist nicht zufällig, darum ist sie eine reine Funktion: Dieselben zwei Flotten ergeben dasselbe Ergebnis, immer und in allen vier Sprachen. Es wird kein Würfel geworfen (anders als in der Lektion über das Kampfsystem, wo jeder Schlag eine Chance hat). So lässt sich die Schlacht mit einem Aufruf und einer Prüfung des Ergebnisses testen. Auch die Piraten aus den Ereignissen kämpfen mit derselben Funktion (gegen eine Flotte ohne Technologien), und eine Entscheidung wie „soll ich angreifen?“ könnte die Schlacht im Kopf durchspielen, auf einer Kopie der Flotten. Die künstliche Intelligenz des Spiels ist einfacher: Sie vergleicht die Mächte der Flotten (wie oben), und wie sie entscheidet, ist das Thema der Lektion über die Rivalen.
Die Runden sind höchstens sechs (BATTLE_ROUNDS): Können sich die beiden Flotten nicht vernichten, zum Beispiel eine kleine Flotte gegen eine große Festung, endet der Kampf ohne Sieger, und das Spiel schreibt „Kein Sieger nach sechs Runden“. So hängt eine Schlacht nie ewig.
Die Idee
Auch die Züge gleichzeitig
Dasselbe Prinzip gilt für die Bewegung. Im Spiel gibt der Spieler (und die künstliche Intelligenz) Befehle, zum Beispiel go 1 2 3 2 2 0 („von System 1 nach 2: drei Abfangjäger, zwei Bomber, zwei Fregatten“), und die Flotten brechen erst in der Bewegungsphase auf. Sie fliegen eine Verbindung pro Zug auf dem kürzesten Weg, und der Befehl erneuert sich selbst, bis sie das Ziel erreichen.
Die Bewegung hat zwei Phasen: Erst verlassen alle ihre Systeme, dann kommen alle an. Darum treffen sich zum Beispiel zwei feindliche Flotten, die ihre Systeme tauschen, nicht: Während die eine aufbricht, ist die andere noch nicht angekommen. Die Schlachten kommen danach, nur dort, wo nach der Bewegung beide Flotten im selben System stehen.
Das Programm bewegt drei Flotten (X, Y und Z) auf einer Kette von Systemen 0, 1, 2, 3 (Befehle nach 3, 0 und 1) zwei Züge lang und druckt, wo sie stehen und wer sich begegnet. Danach macht es dasselbe falsch: Es bewegt Flotte für Flotte und prüft nach jeder einzelnen Bewegung sofort auf eine Begegnung.
function stepToward(pos, dest) { if (pos < dest) return pos + 1; if (pos > dest) return pos - 1; return pos;}function report(turn, names, pos) { const parts = pos.map((p, i) => names[i] + " bei " + p); console.log("Zug " + turn + ": " + parts.join(", ")); for (let s = 0; s < 4; s++) { const here = names.filter((_, i) => pos[i] === s); if (here.length > 1) console.log("Begegnung bei " + s + ": " + here.join(", ")); }}const names = ["X", "Y", "Z"];const dest = [3, 0, 1];let pos = [0, 1, 2];for (let turn = 1; turn <= 2; turn++) { const next = pos.map((p, i) => stepToward(p, dest[i])); pos = next; report(turn, names, pos);}const live = [0, 1, 2];for (let i = 0; i < live.length; i++) { live[i] = stepToward(live[i], dest[i]); for (let j = 0; j < live.length; j++) { if (j !== i && live[j] === live[i]) console.log("Einzeln nacheinander: " + names[i] + " trifft " + names[j]); }}Zug 1: X bei 1, Y bei 0, Z bei 1Begegnung bei 1: X, ZZug 2: X bei 2, Y bei 0, Z bei 1Einzeln nacheinander: X trifft YEinzeln nacheinander: Z trifft XIm ersten Zug tauschen X (von 0 nach 3) und Y (von 1 nach 0) die Systeme und begegnen sich nicht, aber X und Z (von 2 nach 1) stehen gleichzeitig in System 1: eine Begegnung. Der falsche Weg sieht auch zwischen X und Y eine „Begegnung“: X kommt in 1 an, während Y noch dort ist und gerade aufbricht. Die Prüfung in der Schleife sieht einen halben Zug. Der richtige Weg berechnet zuerst, wo jeder stehen wird (next), aus dem alten Zustand, und setzt sie erst dann dorthin (pos = next).
Hier ist die Bewegung im Spiel. Die Liste moving ist „in der Luft“: Die Schiffe haben ihre Flotte schon verlassen, sind aber noch nicht angekommen:
// Eine Runde der Galaxie hat Phasen: Diplomatie der künstlichen Intelligenz, ihre Aufträge, Bewegung aller// Flotten auf einmal, Kämpfe, Eroberung, Produktion und Forschung, ein Ereignis, Ende der Runde.// Die Bewegung ist gleichzeitig: zuerst verlassen alle Schiffe ihre Systeme, erst dann kommen sie an.function resolveMovement(world) { const moving = []; for (const empire of world.empires) { if (!empire.alive) continue; for (const order of empire.orders) { const fleet = fleetAt(world, empire.id, order.from); if (fleet === null) continue; const ships = [0, 0, 0, 0]; for (let k = 0; k < SHIP_KIND_COUNT; k++) { ships[k] = minOf(order.ships[k], fleet.ships[k]); fleet.ships[k] -= ships[k]; } if (totalOf(ships) > 0) moving.push({ owner: empire.id, to: order.to, dest: order.dest, ships: ships }); } empire.orders = []; } for (const move of moving) { addShips(world, move.owner, move.to, move.ships); // der lange Weg geht von selbst weiter: ein neuer Auftrag von dem Ort, wo sie angekommen sind if (move.to !== move.dest) { world.empires[move.owner].orders.push({ from: move.to, to: nextStep(world.systems, move.to, move.dest), dest: move.dest, ships: move.ships.slice() }); } } removeEmptyFleets(world);}Die Idee
Eine Schlacht simulieren
Die Schlacht ist eine reine Funktion, darum musst du sie nicht ansehen, um sie zu verstehen: Du kannst sie auf vielen Flotten laufen lassen und eine Tabelle lesen. So wird die Balance des Spiels eingestellt. Das Programm vergleicht gleiche Flotten von je 6 Schiffen auf mehreren Rollenpaaren:
const pairs = [[0, 1], [1, 2], [0, 2], [0, 3]];for (const [first, second] of pairs) { const a = new Array(KINDS).fill(0); const b = new Array(KINDS).fill(0); a[first] = 6; b[second] = 6; const head = countsText(a) + " gegen " + countsText(b) + ": "; runBattle(a, b, "a", "b", []); if (total(a) > 0 && total(b) === 0) console.log(head + "gewinnt " + LETTERS[first] + ", übrig " + countsText(a)); else if (total(b) > 0 && total(a) === 0) console.log(head + "gewinnt " + LETTERS[second] + ", übrig " + countsText(b)); else console.log(head + "kein Sieger");}6I gegen 6B: gewinnt I, übrig 3I6B gegen 6F: gewinnt B, übrig 4B6I gegen 6F: gewinnt F, übrig 6F6I gegen 6D: gewinnt D, übrig 6DDas Dreieck zeigt sich in den Zahlen. Sechs Abfangjäger schlagen sechs Bomber und verlieren drei. Sechs Bomber schlagen sechs Fregatten und verlieren zwei. Sechs Fregatten vernichten sechs Abfangjäger ohne einen einzigen Verlust: Die Abfangjäger fallen schon in der ersten Runde. Das Schlachtschiff (Hülle 90, Angriff 18) ist ein anderer Fall: Sechs Schlachtschiffe vernichten sechs Abfangjäger, ebenfalls ohne Verlust, weil jedes von ihnen viel mehr aushält (im Spiel braucht es eine Technologie und mehr Ressourcen). „Was ist stärker?“ hat keine einzige Antwort: Es hängt von der Rolle des Feindes ab. Darum ist auch die Macht einer Flotte (die Summe der Mächte der Rollen) nur eine grobe Schätzung.
Das Spiel macht dasselbe mit ganzen Spielen: --simulate=200 lässt zweihundert Spiele ohne Bildschirm laufen und druckt, wer gewinnt. Das ist das Thema der letzten Lektion. Die Schlachten darin sind diese Funktion, tausendfach ausgeführt.
Probier es aus
Sage die Ausgabe voraus
Drei Bomber gegen drei Abfangjäger. Wer gewinnt, und wie viele Runden dauert die Schlacht? Starte das Programm nicht: Rechne die Runden von Hand mit den Tabellen der Lektion aus (Hülle 10 und Angriff 5 für den Abfangjäger, 16 und 7 für den Bomber) und vergiss die Wunden nicht.
const a = [0, 3, 0, 0, 0];const b = [3, 0, 0, 0, 0];const log = [];runBattle(a, b, "Terranisches Reich", "Orion-Hegemonie", log);for (const line of log) console.log(line);Runde 1: Die Abfangjäger treffen die Bomber mit 175%: 3 × 5 × 175 / 100 = 26, und 26 / 16 = 1 fällt, eine Wunde von 10 bleibt. Die Bomber treffen die Abfangjäger mit 50%: 3 × 7 × 50 / 100 = 10, ein Abfangjäger fällt ohne Wunde. Runde 2 (2 gegen 2): 2 × 5 × 175 / 100 = 17 plus die Wunde 10 ergibt 27, ein Bomber fällt (eine Wunde von 11 bleibt); die Bomber richten 7 an, kein Abfangjäger fällt (Wunde 7). Runde 3 (1 Bomber mit Wunde 11 gegen 2 Abfangjäger mit Wunde 7): 17 + 11 = 28 tötet den letzten Bomber, und 3 + 7 = 10 tötet einen Abfangjäger. Ein Abfangjäger bleibt. Die Ausgabe mit vier Runden ergibt sich, wenn die zweite Salve nach der ersten berechnet wird (die Bomber, die zuerst schießen, würden zuerst treffen): Es werden vier Runden, und der Rest ist gleich. Die Ausgabe mit zwei überlebenden Abfangjägern ergibt sich ohne Übertragen der Wunden: Langsames Feuer tötet nicht.
Probier es aus
Finde den Fehler
Ein Kollege wollte die Runde „aufräumen“: Sobald die Salve von a berechnet ist, wendet er sie sofort an und berechnet erst danach die Salve von b. So braucht er die Kopien für die Salven nicht. Ein Duell von zwei Abfangjägern gegen zwei Bomber druckt:
const a = [2, 0, 0, 0, 0];const b = [0, 2, 0, 0, 0];const log = [];runBattle(a, b, "a", "b", log);for (const line of log) console.log(line);Runde 1: 2I | 2B Verluste 0 / 1Runde 2: 2I | 1B Verluste 0 / 1Sieg für a. Übrig: 2I.Die zweite Runde endet mit „Verluste 0 / 1“, und der Sieg lässt zwei Abfangjäger übrig. Vor der Änderung lautet die zweite Runde „Verluste 1 / 1“, und es bleibt ein Abfangjäger (Runde 2: 2I | 1B Verluste 1 / 1). Welche Zeile läuft zu früh?
Wähle die Zeile mit dem Fehler.
Das Anwenden des Schadens auf b steht vor der Salve von b: Wenn b schießt, ist seine Zahl schon verringert, und die Bomber, die in dieser Runde fallen, bleiben stumm. In der zweiten Runde wird der letzte Bomber von der Salve von a getötet und kann nicht zurückschlagen. Richtig ist, beide Salven aus den Kopien zu Rundenbeginn zu berechnen und sie erst dann anzuwenden, wie es die Funktion der Schlacht in der Lektion tut.
Genauer
Was dieses System offenlässt
Die Schlacht in der Lektion ist der Kern; im Spiel gibt es noch einiges, das am Prinzip nichts ändert.
Technologien geben Boni auf Angriff und Hülle (und zusätzliche für die Bomber und für die Fregatten). Sie kommen über stats von volley und applyDamage hinein: Dasselbe Array mit Schiffszahlen kämpft für zwei Imperien unterschiedlich. Darum gibt es den Parameter im Kanon und hier nicht.
Festungen sind eine fünfte Rolle (FORT_KIND): Die Planeten eines Imperiums in einem System geben ihm Stationen, die schießen und wie Schiffe fallen. Nach der Schlacht bleiben die überlebenden Stationen bis zum Ende des Zugs erhalten, und zu Beginn des nächsten sind sie wieder voll.
Mehr als zwei Imperien in einem System kämpfen paarweise: Für jedes Paar im Krieg, das dort Schiffe oder Stationen hat, wird eine Schlacht aufgerufen. Die Reihenfolge der Paare ist fest, darum ist das Ergebnis bestimmt.
Es gibt keinen Rückzug mitten in der Schlacht und keine Zielwahl. Ein Rückzug ist ein Bewegungsbefehl, der vor der Schlacht ausgeführt wird. Eine Gruppe schießt immer auf die günstigste Rolle; eine reichere Version würde dem Spieler einen Befehl „Fokus auf eine Rolle“ geben, aber dann wäre die Schlacht nicht mehr allein eine Funktion der zwei Flotten.
Verluste sind ganze Schiffe. Eine Wunde bleibt nur innerhalb der Schlacht: Danach ist ein verwundetes Schiff gesund, weil eine Flotte nur eine Anzahl ist. Ein Spiel mit Reparaturen würde die Wunde an der Flotte zwischen den Zügen behalten.
Übung
Eine Schlacht aus der Eingabe
Löse das in deinem Editor.
Eine Schlacht aus der Eingabe. Das Programm liest aus der Eingabe zwei Zeilen mit je fünf Zahlen: die Anzahl von I, B, F, D und # für die Flotte a, danach dasselbe für b. Es druckt die Zeilen der Schlacht wie in der Lektion: eine Zeile je Runde und die letzte Zeile mit dem Sieg (mit den Namen „a“ und „b“; ohne Überschrift).
Mit der Eingabe 5 0 4 0 0 | 0 6 2 0 0 (je eine Flotte pro Zeile) lautet die Ausgabe:
Runde 1: 5I 4F | 6B 2F Verluste 4 / 2Runde 2: 3I 2F | 4B 2F Verluste 4 / 3Runde 3: 1I | 2B 1F Verluste 1 / 0Sieg für b. Übrig: 2B 1F.Achte auf die dritte Runde: a ist mit einem Abfangjäger gegen zwei Bomber und eine Fregatte übrig, und der Sieg gehört b, nicht a, obwohl a mit mehr Schiffen beginnt.
Weitere Aufgaben
- Wie viele Abfangjäger braucht es? Ab wie vielen Abfangjägern (von 1 bis 30) schlagen sie 4 Fregatten? Drucke die erste Zahl, bei der a gewinnt. Probiere es auch mit Bombern gegen 4 Fregatten.
- Reihenfolge tauschen. Lass dieselbe Schlacht als (a, b) und als (b, a) laufen. Ist das Ergebnis dasselbe? Warum?
- Schlachtschiff. Gib a ein Schlachtschiff. Wie viele Abfangjäger braucht b, um es zu vernichten?
- Leere Flotte. Was macht das Programm, wenn eine Seite keine Schiffe hat? Es darf nicht abstürzen.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Funktionen · Parameter und return · static: was alle Objekte teilen · Arrays: push, splice und sort
- Aufgaben zum Thema: Epic Cosmos: das Imperium