Wie du über ein Spiel nachdenkst
Kurz gesagt: Ein Spiel besteht aus Zustand, Regeln, Eingabe, Schleife und Ende; Zufall mit Startwert macht es testbar, und die Logik steckt in Methoden, die einen Wert zurückgeben und sich testen lassen.
Problem
Wo fängt ein Spiel an?
Du willst ein Spiel schreiben. Zum Beispiel ein Würfelrennen: Eine Figur startet auf Feld 0, du würfelst, und sie rückt vor, bis sie Feld 20 erreicht.
Du öffnest den Editor und ... was schreibst du zuerst? Das Menü? Den Würfel? Das Spielbrett? Wenn du mit allem gleichzeitig anfängst, hast du nach einer Stunde viel Code, der immer noch nicht läuft.
Es hilft, dem Spiel fünf Fragen zu stellen, bevor du Code schreibst.
Die Idee
Fünf Fragen an jedes Spiel
- Zustand. Welche Zahlen beschreiben die Welt gerade? Im Rennen sind es das Feld der Figur und die Nummer des Zugs. Im Schach ist es die Position jeder Figur und wer am Zug ist.
- Regeln. Was ist erlaubt, und was passiert danach? „Die Figur rückt so viele Felder vor, wie der Würfel zeigt.“
- Eingabe. Was kommt von außen? Die Wahl des Spielers oder die Zahl auf dem Würfel.
- Schleife. Zug um Zug: Eingabe, Zustand aktualisieren, anzeigen. Und wieder von vorn.
- Ende. Wann ist das Spiel aus, und wer gewinnt? „Die Figur steht auf Feld 20.“
Die Antworten sind fast schon der fertige Code: Der Zustand sind Variablen, die Regeln sind Methoden, die Eingabe ist Console.ReadLine() oder Random, die Schleife ist while oder for, und das Ende ist die Bedingung der Schleife.
Sieh es dir an
Zug um Zug
Random random = new Random(42);int field = 0;for (int turn = 1; field < 20; turn++){ int roll = random.Next(1, 7); field = Math.Min(field + roll, 20); Console.WriteLine("Zug " + turn + ": Wurf " + roll + ", Feld " + field);}Console.WriteLine("Ziel! Die Figur hat Feld 20 erreicht.");Schritt für Schritt, in Worten
- Ein Generator mit Startwert 42 und eine Figur auf Feld 0.
- Zug 1: Die Eingabe ist ein Wurf von 5, der Zustand wird zu Feld 5, der Bildschirm zeigt die Zeile.
- Zug 2: Die Eingabe ist ein Wurf von 1, der Zustand wird zu Feld 6, der Bildschirm zeigt die Zeile.
- Zug 3: Die Eingabe ist ein Wurf von 1, der Zustand wird zu Feld 7, der Bildschirm zeigt die Zeile.
- Zug 4: Die Eingabe ist ein Wurf von 4, der Zustand wird zu Feld 11, der Bildschirm zeigt die Zeile.
- Zug 5: Die Eingabe ist ein Wurf von 2, der Zustand wird zu Feld 13, der Bildschirm zeigt die Zeile.
- Zug 6: Die Eingabe ist ein Wurf von 2, der Zustand wird zu Feld 15, der Bildschirm zeigt die Zeile.
- Zug 7: Die Eingabe ist ein Wurf von 5, der Zustand wird zu Feld 20, der Bildschirm zeigt die Zeile. Feld 20: Die Bedingung field < 20 ist jetzt falsch.
- Die Schleife ist zu Ende, weil das Feld 20 erreicht hat. Das ist das Ende des Spiels.
Das ganze Spiel ist eine einzige Schleife. In jedem Zug kommt eine Eingabe (der Würfel), der Zustand wird aktualisiert (das neue Feld) und angezeigt (eine Zeile auf dem Bildschirm). Die Bedingung der Schleife ist das Ende: Solange das Feld unter 20 liegt, gibt es einen weiteren Zug.
Math.Min hält die Figur auf dem Brett: Schickt der Würfel sie über 20 hinaus, bleibt sie auf 20 stehen.
Genauer, für Neugierige
- Dieses Spiel ist rundenbasiert: Nichts passiert, bis die nächste Eingabe kommt. Schach, Kartenspiele und die meisten Spiele in diesem Abschnitt funktionieren so.
- Echtzeitspiele (Shooter, Autorennen) haben ebenfalls eine Schleife, aber sie wartet nicht auf den Spieler. Sie läuft 60-mal pro Sekunde: Sie liest die Tasten, die gerade gedrückt sind, bewegt alles um die vergangene Zeit weiter und zeichnet das Bild. Auch wenn der Spieler nichts drückt, bewegt sich die Welt.
Die Idee
Gleicher Startwert, gleiches Spiel
Wenn die Würfe wirklich zufällig sind, ist jeder Lauf ein anderes Spiel. Wie prüfst du dann, ob das Spiel funktioniert? Du weißt nicht, was du erwarten sollst.
Deshalb bekommt Random in diesen Lektionen eine Startzahl, einen Startwert (englisch: Seed). Derselbe Startwert liefert jedes Mal dieselbe Folge:
Random first = new Random(42);Random second = new Random(42);for (int i = 0; i < 5; i++) { int a = first.nextInt(6) + 1; int b = second.nextInt(6) + 1; System.out.println(a + " und " + b);}3 und 34 und 41 und 13 und 31 und 1Random first = new Random(42);Random second = new Random(42);for (int i = 0; i < 5; i++){ int a = first.Next(1, 7); int b = second.Next(1, 7); Console.WriteLine(a + " und " + b);}5 und 51 und 11 und 14 und 42 und 2from random import Random
first = Random(42)second = Random(42)for i in range(5): a = first.randint(1, 6) b = second.randint(1, 6) print(f"{a} und {b}")6 und 61 und 11 und 16 und 63 und 3function makeRandom(seed) { return () => { seed = (seed + 0x9e3779b9) | 0; let z = seed; z = Math.imul(z ^ (z >>> 16), 0x85ebca6b); z = Math.imul(z ^ (z >>> 13), 0xc2b2ae35); return ((z ^ (z >>> 16)) >>> 0) / 4294967296; };}
const first = makeRandom(42);const second = makeRandom(42);for (let i = 0; i < 5; i++) { const a = Math.floor(first() * 6) + 1; const b = Math.floor(second() * 6) + 1; console.log(`${a} und ${b}`);}2 und 26 und 61 und 15 und 51 und 1Die beiden Generatoren mit 42 liefern dieselben Zahlen. Starte das Programm noch einmal, und sie sind wieder gleich. Deshalb haben die Beispiele und Aufgaben hier eine genaue Ausgabe, die sich prüfen lässt.
Sieh dir die Ausgabe auch in den anderen Tabs an. Jede Sprache hat ihren eigenen Generator: Aus demselben Startwert entstehen andere Zahlen. Deshalb haben Beispiele mit Zufallszahlen für jede Sprache eine eigene Ausgabe.
Genauer, für Neugierige
- Wenn das Spiel für echte Spieler fertig ist, entfernst du den Startwert:
new Random(). Dann wählt der Generator den Startwert selbst anhand der Uhr, und jedes Spiel ist anders.
- Üblich ist, den Startwert am Anfang des Spiels auszugeben. Findet ein Spieler einen Fehler, kannst du mit seinem Startwert genau dasselbe Spiel nachspielen.
Die Idee
Zuerst die kleinste laufende Version
Das Programm aus der Szene ist ein ganzes Spiel in etwa zehn Zeilen. Es hat keinen zweiten Spieler, kein Menü und keine Regel außer „vorwärts“. Aber es läuft von Anfang bis Ende und zeigt, dass Schleife und Ende stimmen.
Von dort wächst das Spiel Regel für Regel. Die zweite Regel: Die Figur muss genau auf Feld 20 landen. Schickt der Würfel sie weiter, springt sie um so viele Felder zurück, wie sie darüber hinaus war. Von 17 mit einer 5 kommt sie auf 20 und geht zurück auf 18.
Eine neue Regel kommt erst dazu, wenn die vorige funktioniert. Geht etwas kaputt, weißt du: Es liegt an dem, was du zuletzt geschrieben hast.
Die Idee
Die Logik gibt zurück, der Bildschirm gibt aus
Die Regel „genau 20“ steckt in einer Methode, die das neue Feld zurückgibt. Das Ende ist eine Methode, die true oder false zurückgibt:
static int move(int field, int roll) { int next = field + roll; if (next > 20) { return 20 - (next - 20); } return next;}
static boolean isFinished(int field) { return field == 20;}
Random random = new Random(7);int field = 0;int turn = 0;while (!isFinished(field)) { turn++; int roll = random.nextInt(6) + 1; field = move(field, roll); System.out.println("Zug " + turn + ": Wurf " + roll + ", Feld " + field);}System.out.println("Genau 20 nach " + turn + " Zügen");Zug 1: Wurf 5, Feld 5Zug 2: Wurf 3, Feld 8Zug 3: Wurf 4, Feld 12Zug 4: Wurf 5, Feld 17Zug 5: Wurf 5, Feld 18Zug 6: Wurf 5, Feld 17Zug 7: Wurf 5, Feld 18Zug 8: Wurf 6, Feld 16Zug 9: Wurf 1, Feld 17Zug 10: Wurf 1, Feld 18Zug 11: Wurf 1, Feld 19Zug 12: Wurf 1, Feld 20Genau 20 nach 12 Zügenstatic int Move(int field, int roll){ int next = field + roll; if (next > 20) { return 20 - (next - 20); } return next;}
static bool IsFinished(int field){ return field == 20;}
Random random = new Random(7);int field = 0;int turn = 0;while (!IsFinished(field)){ turn++; int roll = random.Next(1, 7); field = Move(field, roll); Console.WriteLine("Zug " + turn + ": Wurf " + roll + ", Feld " + field);}Console.WriteLine("Genau 20 nach " + turn + " Zügen");Zug 1: Wurf 3, Feld 3Zug 2: Wurf 6, Feld 9Zug 3: Wurf 4, Feld 13Zug 4: Wurf 1, Feld 14Zug 5: Wurf 3, Feld 17Zug 6: Wurf 5, Feld 18Zug 7: Wurf 1, Feld 19Zug 8: Wurf 6, Feld 15Zug 9: Wurf 6, Feld 19Zug 10: Wurf 6, Feld 15Zug 11: Wurf 3, Feld 18Zug 12: Wurf 6, Feld 16Zug 13: Wurf 3, Feld 19Zug 14: Wurf 1, Feld 20Genau 20 nach 14 Zügendef move(field, roll): new_field = field + roll if new_field > 20: return 20 - (new_field - 20) return new_field
def is_finished(field): return field == 20
random = Random(7)field = 0turn = 0while not is_finished(field): turn += 1 roll = random.randint(1, 6) field = move(field, roll) print(f"Zug {turn}: Wurf {roll}, Feld {field}")print(f"Genau 20 nach {turn} Zügen")Zug 1: Wurf 3, Feld 3Zug 2: Wurf 2, Feld 5Zug 3: Wurf 4, Feld 9Zug 4: Wurf 6, Feld 15Zug 5: Wurf 1, Feld 16Zug 6: Wurf 1, Feld 17Zug 7: Wurf 5, Feld 18Zug 8: Wurf 1, Feld 19Zug 9: Wurf 3, Feld 18Zug 10: Wurf 5, Feld 17Zug 11: Wurf 1, Feld 18Zug 12: Wurf 5, Feld 17Zug 13: Wurf 2, Feld 19Zug 14: Wurf 1, Feld 20Genau 20 nach 14 Zügenfunction move(field, roll) { const next = field + roll; if (next > 20) { return 20 - (next - 20); } return next;}
function isFinished(field) { return field === 20;}
const random = makeRandom(7);let field = 0;let turn = 0;while (!isFinished(field)) { turn++; const roll = Math.floor(random() * 6) + 1; field = move(field, roll); console.log(`Zug ${turn}: Wurf ${roll}, Feld ${field}`);}console.log(`Genau 20 nach ${turn} Zügen`);Zug 1: Wurf 1, Feld 1Zug 2: Wurf 3, Feld 4Zug 3: Wurf 6, Feld 10Zug 4: Wurf 4, Feld 14Zug 5: Wurf 3, Feld 17Zug 6: Wurf 2, Feld 19Zug 7: Wurf 5, Feld 16Zug 8: Wurf 2, Feld 18Zug 9: Wurf 2, Feld 20Genau 20 nach 9 ZügenWeder Move noch IsFinished gibt etwas aus. Sie rechnen nur. Die Ausgabe steht in Main.
Warum so? Eine Methode, die eine Zahl zurückgibt, kannst du mit beliebigen Werten aufrufen und das Ergebnis prüfen: Move(17, 5) muss 18 sein. Eine Methode, die ausgibt, prüfst du nur mit den Augen. Und wenn das Spiel morgen von der Konsole in ein Fenster umzieht, bleiben die Regeln gleich; nur die Anzeige ändert sich.
Probier es aus
Wo landet die Figur?
Was gibt das Programm aus?
static int Move(int field, int roll){ int next = field + roll; if (next > 20) { return 20 - (next - 20); } return next;}
static bool IsFinished(int field){ return field == 20;}
Console.WriteLine(Move(10, 4));Console.WriteLine(Move(17, 3));Console.WriteLine(Move(18, 5));10 + 4 = 14. 17 + 3 = 20, genau. 18 + 5 = 23 liegt 3 über 20, also springt die Figur auf 20 − 3 = 17 zurück.
Probier es aus
Finde den Fehler
Die Figur sollte vorrücken, aber das Programm gibt in jedem Zug Feld 0 aus:
Zug 1: Wurf 3, Feld 0Zug 2: Wurf 6, Feld 0Zug 3: Wurf 4, Feld 0In welcher Zeile steckt der Fehler?
Wähle die Zeile mit dem Fehler.
Move gibt das neue Feld zurück, aber das Ergebnis wird weggeworfen. Die Methode kann das field des Aufrufers nicht ändern: Der Parameter ist eine Kopie des Werts. Du brauchst field = Move(field, roll);. Steht eine Zeile wie diese in while (!isFinished(field)), endet das Spiel nie.Probier es aus
Wann endet das Spiel?
Was fehlt, damit das Ende genau auf Feld 20 liegt?
static bool IsFinished(int field)
{
return field 20;
}== vergleicht. Ein einzelnes = ist eine Zuweisung und lässt sich hier nicht kompilieren. >= funktioniert ebenfalls, weil Move nie mehr als 20 zurückgibt, aber == sagt die Regel klarer: Das Ende liegt genau auf 20.Übung
Drei kleine Spiele
Löse diese Aufgaben in deinem Editor. Die Logik steckt in Methoden, die einen Wert zurückgeben, und die Ausgabe steht in Main.
1. Ein Rennen für zwei. Anna und Jonas würfeln abwechselnd, mit new Random(3). Es gilt die Regel „genau 20“, mit Move aus der Lektion. Ihre Felder stehen in einem Array int[2]. Wer zuerst 20 erreicht, gewinnt.
Anna: Wurf 2, Feld 2Jonas: Wurf 5, Feld 5Anna: Wurf 6, Feld 8Jonas: Wurf 2, Feld 7Anna: Wurf 4, Feld 12Jonas: Wurf 2, Feld 9Anna: Wurf 2, Feld 14Jonas: Wurf 6, Feld 15Anna: Wurf 3, Feld 17Jonas: Wurf 3, Feld 18Anna: Wurf 4, Feld 19Jonas: Wurf 5, Feld 17Anna: Wurf 5, Feld 16Jonas: Wurf 1, Feld 18Anna: Wurf 1, Feld 17Jonas: Wurf 6, Feld 16Anna: Wurf 1, Feld 18Jonas: Wurf 6, Feld 18Anna: Wurf 5, Feld 17Jonas: Wurf 3, Feld 19Anna: Wurf 2, Feld 19Jonas: Wurf 4, Feld 17Anna: Wurf 3, Feld 18Jonas: Wurf 1, Feld 18Anna: Wurf 6, Feld 16Jonas: Wurf 3, Feld 19Anna: Wurf 4, Feld 20Anna gewinnt2. Zahl raten. Das Programm wählt heimlich eine Zahl von 1 bis 100 mit new Random(2026). Der Spieler tippt Vermutungen ein, eine pro Zeile. Nach jeder sagt das Programm „Höher“, „Niedriger“ oder „Richtig“. Am Ende sagt es, wie viele Versuche es gebraucht hat. Der Hinweis kommt aus einer Methode string Hint(int guess, int secret), die den Text zurückgibt.
502512181650: Niedriger25: Niedriger12: Höher18: Niedriger16: RichtigErraten in 5 VersuchenDie Eingabe ist in jeder Sprache anders, weil ihre Generatoren aus demselben Startwert eine andere Geheimzahl wählen.
3. Wer hat Tic-Tac-Toe gewonnen? Die erste Zeile ist die Anzahl der Bretter. Jedes Brett besteht aus drei Zeilen zu je drei Zeichen: X, O oder . für ein leeres Feld. Schreibe eine Methode char Winner(char[,] board), die X, O oder . zurückgibt, wenn es keine drei in einer Zeile, Spalte oder Diagonale gibt.
3XOXOXOOOXXXOOOOX.XXOXXOOOXXX gewinntO gewinntKein SiegerWeitere Übungen
- Fallen. Feld 13 schickt die Figur zurück auf Feld 5. Füge die Regel zu
Movehinzu und lass das Rennen noch einmal laufen. - Startwert ausgeben. Mach den Startwert zu einer Variablen, gib ihn am Anfang aus und lass das Spiel mit drei verschiedenen Startwerten laufen.
- Tic-Tac-Toe für zwei. Die Spieler tippen Zeile und Spalte ihres Zugs ein. Zeige nach jedem Zug das Brett und prüfe mit
Winner, ob das Spiel zu Ende ist.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Zweidimensionale Arrays · Strings: mit Text arbeiten · Methoden · Parameter und return
- Aufgaben zum Thema: Wir bauen ein Spiel