Gamedesign Tutorial "Snail Race!"

(ab 9 Jahren)

reinhard@finalmedia.de
Sa 28. Jun 09:46:00 CEST 2025
Play in Browser! (Benötigt Tastatur)

Tipp: Wenn du das Spiel im Browser startest, steht dir auch dort die ganze Entwicklungs-Plattform tic80 zur Verfügung. Drücke ESC um ins Menü zu gelangen, wähle "Close Game" mit Enter. Drücke danach nochmal ESC, um nun in den Code-Editor zu gelangen. Du kann den Code dann live ändern und mit STRG+R die Fantasy Console jederzeit wieder (mit geändertem Code) starten. Bedenke aber: Ein wirkliches Speichern des geänderten Quellcodes ist dort nicht möglich. Änderungen gehen also verloren.

Das Spielkonzept

Grundlagen

Präambel

Gute Computerspiele haben die nachfolgenden Eigenschaften, die beim Gamedesign von Anfang an berücksichtigt werden sollten:

Ein Tutorial zum Gamedesign wiederum soll mehrere didaktische Ziele verfolgen.

1. Didaktisches Ziel +++ Praktik +++ definieren:

Wir möchten spielerisch Schnellschreiben auf der Tastatur trainieren.

Desweiteren soll die eigene Kreativität gefördert werden. Die Spielentwicklung soll also ausbaufähig sein und der Schüler soll eigene Ideen und Verbesserungen einbringen können. Daher darf das Spiel in der Referenzimplementierung nicht überfrachtet sein. Das Konzept soll viel Raum für eigene Ideen lassen.

Die Referenzimplementierung muss überschaubar und grafisch einfach konzipiert sein.

2. Didaktisches Ziel +++ Theorie +++ definieren:

Wissen und Methodik aus der Informatik vermitteln.

Wir möchten zudem ein Verständnis von scheinbarer einfacher Intelligenz mittels der Zufallsfunktion (random) vermitteln. Ebenso vermitteln wir den Unterschied zwischen lokalen und globalen Variablen.

Bzgl. der Programmiersprache lua verwenden wir die Basisoperatoren und Stringoperatoren wie Konkatenierung mittels "..", sowie substring Funktion zum Zugriff auf einzelne Stellen der Zeichenfolge. Der Operator "#" wird benutzt, um die Länge der Zeichenfolge zu erfragen.

Zudem nutzen wir Grundlagen der Softwarearchitektur, Designentscheidung, das KISS-Prinzip (Halte es einfach) und das Überladen von Variablen zur Mehrfachbedeutung.

Das Konzept enthält darüberhinaus by Design ein EasterEgg, bzw. einen "gewollten Bug", der einem Player zufällig Vorteile verschaffen kann. Der Schüler soll dies selbst herausfinden.

Spoiler: Zwei zufällig aufeinanderfolgende gleiche Buchstaben in der Zeichenkette geben dem jeweiligen Spieler einen Vorteil, da er sie "mit einem Haps" in einem Zug wegfrühstückt, da das Timing 60fps läuft und key autorepeat der Plattform greift.

Implementierung

Designvorgaben

## Verbindliche Designvorgaben zur Implementierung

- Es soll eine Maximalanzahl von 4096 Punkten geben. 
- Es soll mind. 3 Spieler geben
- Es soll von diesen 3 Spielern mind. einen Computergegner geben
- Es soll mind. 5 Schwierigkeitsgrade geben, die den Computergegner besser oder schlechter machen
- Jeder Buchstabe aus dem Pool des Spielers soll für den Spieler exakt 3 mal vorkommen
- Es soll einen Gesamtpool aus nur 24 Buchstaben geben
- Die Spieler sollen optisch gut unterscheidbar sein

Plattformvorgaben

Wir programmieren in der Programmiersprache "lua" auf der Plattform "TIC-80", einer Fantasy Konsole.

Diese Plattform/IDE bringt auch einen integrierten Pixel-Grafikeditor mit, damit der Schüler die Protagonisten grafisch selbst gestalten kann. Und so ein spielerischer Einstieg gegeben ist, bevor man sich dem Quellcode widmet.

## Verbindliche Plattformvorgaben zur Implementierung

- Grafik der Spieler sollen als 8x8 Pixelgrafik in den tile Slots 49,50,51 definiert werden. 
  (Wir halten uns die echten Sprite Slots frei für spätere Implementierung von animierten Sprites)
- Zur Gestaltung kann darf der integrierte Editor verwendet werden. Beispiel:
## Grafik der Referenzimplementierung

Spieler 1:  Tile 49   8x8 Pixel
Spieler 2:  Tile 50   8x8 Pixel
Spieler 3:  Tile 51   8x8 Pixel

Referenzimplementierung


## Der Quellcode meiner Referenzimplementierung

-- title:   snail race
-- author:  reinhard@finalmedia.de
-- desc:    tiny snail race keyboard typing trainer
-- site:    finalmedia.de
-- license: CC-BY-SA
-- version: 0.1
-- script:  lua


-- globale variablen definieren
t=0
y=56
x=16
maxscore=4096
handicap=50
level=2
step=6
a1="qweasdyx"
a2="rtzfghvb"
a3="uiopjklm"
gameover=1
winner=0


function shuffle(str)
	local chars = {}
	for i=1, #str do
		chars [i] =str:sub(i,i)
        end
	for i = #chars, 2, -1 do
		local j = math.random(i)
		chars[i], chars[j] = chars[j], chars[i]
        end                                                                                                                 
        return table.concat(chars)
end                      


function newgame()
	gameover=0
	score=maxscore
	winner=0
	t=0                                                                                         
	x1=x
	x2=x
	x3=x
	s1=""
	s2=""
	s3=""

	for i=0,level,1 do
		s1=s1..shuffle(a1)
		s2=s2..shuffle(a2)
		s3=s3..shuffle(a3)
	end
end


function winspr(player)
	winner=player
	gameover=t
end



function BOOT()
	newgame()
end


function TIC()                     

	-- hintergrund farbe setzen
        cls(0)

	-- alle drei schnecken zeichnen
	spr(49,x1,y,0,2,0,0,1,1)
	spr(50,x2,y+10,0,2,0,0,1,1)
	spr(51,x3,y+20,0,2,0,0,1,1)

	-- titel und anleitung zeichnen
        print("SNAIL RACE!",1,9,12)
        print("Hold enter to generate new seed.",1,18,12)
        print("Press your current letter key to eat.",1,27,12)
        print("Press 0-5 to choose handicap.",1,36,12)
        print("Computer player is green.",1,45,12)

        -- jede essenspur aus buchstaben zeichnen
        print(s1,x1+20,y+10)
	print(s2,x2+20,y+20)
        print(s3,x3+20,y+30)

	-- entertaste = neues spiel
        if (key(50)) then newgame() end

        -- zifferntasten schwierigkeitsgrad
	if (key(27)) then handicap=99 end	-- easy
	if (key(28)) then handicap=50 end	-- default
	if (key(29)) then handicap=40 end
	if (key(30)) then handicap=30 end
	if (key(31)) then handicap=20 end
	if (key(32)) then handicap=10 end	-- insane :)

	-- siegergrafik anzeigen, falls spiel gewonnen
	if (winner>0) then
		spr(winner,90,66,0,6,0,0,1,1)
		print("WINNER! "..score.. " Points",65,120,12)
	end                 

	-- aktuellen ziel-buchstaben jedes players finden
        c1=s1:sub(1,1)
	c2=s2:sub(1,1)
	c3=s3:sub(1,1)


	-- solange noch niemand gewonnen hat
	-- buchstaben eingaben zulassen
	if (gameover<1) then
		for keycode = 1,26 do
			if key(keycode) then
				if ((keycode+96)==c1:byte()) then s1=s1:sub(2); x1=x1+step end
				if ((keycode+96)==c2:byte()) then s2=s2:sub(2); x2=x2+step end
				if ((keycode+96)==c3:byte()) then s3=s3:sub(2); x3=x3+step end
			end
		end
	end

        -- aktuelle score ausgeben
        print(score,1,1)

	-- aktuelles handicap ausgeben
	print(handicap,226,1)

	-- aktuelle buchstaben ausgeben
	print(c1,1,128,10)
	print(c2,10,128,4)
	print(c3,20,128,5)

        -- aktuelle restanzahl ausgeben
        print(#s1,1,y+10,10)
	print(#s2,1,y+20,4)
	print(#s3,1,y+30,5)

	-- gewinner, wenn all buchstaben gegessen
	if (#s1<1) then winspr(49) end
	if (#s2<1) then winspr(50) end
	if (#s3<1) then winspr(51) end

	-- computer player
	if (t>handicap and math.random(handicap)==1 and gameover<1) then
	s3=s3:sub(2)
	x3=x3+step
	end

	-- die zeit vergeht und highscore schwindet
	if (gameover<1) then 
		t=t+1
		if (t < maxscore) then score=score-1 end
	end
end




Weiterführendes

Ausbaumöglichkeiten

Modi implementieren

Chaos Mode

Möglichkeit eines zusätzlichen "Chaosmode": Für Tastatureingabe der realen Player (statt linker und rechter Bereich) einfach aus dem Pool aller Buchstaben, reihum an alle Player einen Buchstaben in dessen pool vergeben. Grafikdesign: z.B. eine Tastatur mit vielen Händen und Fingern über Kreuz. Realisierung ist hier recht einfach, da nur die player pools anders befüllt werden. Das erhöht den Spielspaß bei 3-4 menschlichen Spielern, die dann z.B. alle auf einer Tastatur ihre jeweiligen Buchstaben finden müssen und sich dabei die Finger verknoten. Natürlich können jedoch an einen Rechner auch mehrere Tastaturen angeschlossen werden.

Demo Mode

Da wir in den Vorgaben bewusst nicht gefordert haben, dass es mindestens einen menschlichen Spieler geben muss, ist es denkbar, nur Computergegener gegeneinander antreten zu lassen. Das ist dann als ein Demo-Mode zu verstehen, in dem man sich zurücklegen kann und nur zuschaut. Beachte bei der Implementierung eines solchen Demo-Modes, dass jeder Computergegner sein eigenes Zufalls-Timing haben muss und idealerweise auch leichte Schwankungen im jeweiligen Handicap berücksichtig werden! (addition und subtraktion eines kleinen zufallswertes)

Animationen und Grafik

die Sprites können animiert werden, sodass die Schnecke z.B. zu fressen scheint. Zudem kann im Ziel für jede Schnecke eine Häuschen liegen, welches sie dann nach getaner Arbeit aufsetzen kann, um zu ruhen.

Schriftarten (Fonts)

Statt der Standard Systemschrift können Grafikfonts verwendet werden, die neben dem Buchstaben z.B. Früchte zeigen etc. Die Buchstabenspur kann auf diese weise sehr einfach grafisch aufgewertet werden, ohne die Implementierung stark zu verändern. Zudem sind Font-Wechsel möglich. Idealerweise sollten auch nur Schriftarten mit fester Lauflänge verwendet werden, um Renderjumps zu vermeiden.

Multiplayer Ausbau

Mehr als 3 Spieler realisieren. z.B. bis zu 8 pieler, davon dann auch mehrere Computergegner und wählbar (auch via Tables und Object).

Persistente Upgrades

Ggf. auch die Sprites der Spieler änderbar gestalten, sodass sie in jeder gewonnenen Runde "erfolgreicher" aussehen, z.B. Sternchen, Sticker oder Pokale erhalten. Dazu z.B. unterhalb neue Sprites einführen und dann ranking-Variable implementieren, die den sprite index des players shifted.

Sounds

Die TIC-80 Konsole ermöglicht auch die einfache Gestaltung von Sound und Musik für das Spiel, um die Lebendigkeit zu erhöhen.

Codeoptimierung

Durch Einsatz von Tables (Arrays), statt einer dedizierten Variable pro Spieler


Fragen und Antworten

Frage: Wie schnell wächst der Wert der Variable t?
Antwort: Da tic80 mit einer festen Rendering Framerate von 60 fps arbeitet, also die Hauptfunktion TIC() 60 mal pro Sekunde aufgerufen wird, wächst also t auch pro Sekunde um den Wert 60. Dieser Wert kann aber auf Millisekunden-Ebene schwanken, da hier keine Echtzeitverarbeitung stattfindet.

Frage: Wie kann man die Fallunterscheidung bzgl. der maxscore am Ende eleganter und ohne Vergleich mit t oder maxscore schreiben?
Antwort: if(score>1) then score=score-1 end

Frage: Wozu braucht es überhaupt die Zeitvariable (tick) t?
Antwort: Primär Zum verzögerten Einstieg der Computergegner nach handicap, weil der computer sonst immer einen Vorteil hat, bevor der menschliche Spieler überhaupt die Zeichenfolge wahrgenommen haben kann.

Frage: Wie ist die "Intelligenz" des Computergegners realisiert?
Antwort: Bedingt durch das gewählte Handicap wird eine Zufallszahl in einem gewissen kleineren oder größeren Bereich gewählt. Der Computergegner darf immer nur dann voranschreiten, wenn diese Zufallszahl eine 1 ist. Wird also der mögliche Wertebereich insgesamt vergrößert, so sinkt automatisch die Wahrscheinlichkeit eines Treffers. Damit spielt der Computergegner also bei einem hohen Handicap random(99) und damit Zahlen zwischen 0 und 99, letztlich schlechter, als bei einem niedrigen Handicap random(10) und damit Zahlen zwischen 0 und 10. Das Handicap verlängert also unterm Strich elegant die zeitliche Dauer vor dem nächsten Zug des Computergegners.

Frage: Wie entfernt man die erste Stelle einer Zeichenfolge?
Antwort: In tic80 lua am elegantesten mittels string=string:sub(2), was bedeutet dass der string erst ab stelle 2 beginnt, also der Rest übrig bleibt. Und diesen Rest weisen wir dem String wieder selbst zu. So ist es auch in der Referenzimplementierung zu sehen.

Frage: Erkläre die Zeile (keycode+96)==c1:byte()
Antwort: in tic80 bezieht sich die funktion key(1) ja bereits auf buchstabe A, weil tic80 diese Buchstabenfolge schon so vorgibt. Die systemseitigen zeichenfolgen in lua sind jedoch ASCII Zeichenfolgen und in der ASCII Tabelle starten die Kleinbuchstaben an der Dezimalstelle 97 (hexadezimal 0x61). Da angegebene offset setzt also den tic80 keycode auf das ascii byte um und vergleicht. Damit wird die Abfrage realisiert, ob die gedrückte Taste dem ASCII Zeichen des ersten Zeichens in der Zeichenfolge des jeweiligen Spielers entspricht. In Diesem Fall für Spieler 1.

Frage: Erkläre die definierte Funktion winspr()
Antwort: wir unterbrechen den gameloop, indem wir die gameover variable auf die aktuelle zeit setzen. es würde hier genügen, die variable auf 1 zu setzen. wir erhalten uns aber so auch den wert der Gesamtdauer für spätere Erweiterungen. Mittels der spr() funktion wiederum stellen wir den Gewinner dar, allerdings nun 4 fach vergrößert. Der übergabe Wert für die winspr() Funktion ist folglich der Dezimalwert der player grafik, also tiles 49,50,51 in unserer Referenzimplementierung.

Frage: Erkläre die for Schleife in der Funktion newgame()
Antwort: Die Variable level hat den Wert 3. Die Variable i nutzen wir nur als counter von 1 bis 3, womit letztlich die jeweiligen Zeichenfolge-Strings der Spieler dreimalig verlängert werden, jeweils um eine neu gewürfelte sequenz aus dem erlaubten zeichenpool des jeweiligen spielers.

Bonusfragen

Frage: Unter welchen Bedingungen tritt in der Referenzimplementierung bei sehr langer Spielzeit hypothetisch ein Überlauf eines kritischen Wertebereichs ein?
Antwort: t könnte hypothetisch überlaufen, wenn nur menschliche Spieler am Start wären und diese mehr als ein Jahr keinerlei Spieleingaben tätigen würden. Durch die Vorgabe, dass es jedoch mind. einen Computergegener geben muss, tritt dieser Fall nicht, bzw. nur mit extrem niedriger Wahrscheinlichkeit ein. Der Computergegner müsste für den maxint32 überlauf ganze 24 mal über eine Zeit von 2.147.483.647/60 Sekunden = 35791394 Sekunden = 9942 Stunden = 414 Tage lang keine zufällige 1 aus seinem sehr geringer handicap Bereich zwischen 1 und 50 bekommen haben. Was sehr sehr unwahrscheinlich ist. t wird ja nicht weiter inkrementiert, sobald ein Spieler gewonnen hat. Im Falle einer Manipulation des Pseudo-Zufallszahlengenators, und dass TIC-80 für diesen nie die 1 returned, und somit lange Zeit niemand gewinnt, könnte also nach 414 Tagen ein Überlauf stattfinden und das Spiel abstürzen. Vorherige Abstürze aus anderen Gründen sind ggf. wahrscheinlicher ;)


Downloads (Referenzimplementierung)

Native Binaries

Quellcode

sneckweg.tic (Tic80)