Tekst-gebaseerd Maze game

Inleiding
In dit all-in-one tonen we hoe je, stap voor stap, kan komen tot een speelbaar, eenvoudige tekst-gebaseerd spel. We hanteren hierbij de principes van "refactoring": we gaan onze code steeds verbeteren op gebied van leesbaarheid en onderhoudbaarheid. Bij iedere stap zullen we dan ook extra functionaliteit toevoegen.
Het doel is te komen tot een spel waarbij de gebruiker kan wandelen door een kaart. De kaart zelf is dynamisch, bepaalde ruimtes zijn pas toegankelijk wanneer aan bepaalde voorwaarden is voldaan. 
Vereiste kennis
Deze tutorial gaat er van uit dat je volgende zaken beheerst:
Basisprincipes van Arrays, zowel 1D als 2D arrays: aanmaken, waarden toevoegen/uitlezen
Werken met de Console-bibliotheek: in het bijzonder Clear(), SetCursorPosition(), ForeGroundColor en BackGroundColor, Write() vs WriteLine()
Je kan werken met while en for-loops
Je begrijpt de werking van het
outkeyword
Fase 1: Een saai spel
We gebruiken een array van strings om de opeenvolgende kamers te beschrijven. Door middel van een for-loop doorlopen we de array en tonen we iedere beschrijving van de kamer op het scherm.
Telkens de gebruiker op enter drukt verschijnt de volgende kamer.
Merk op dat de array-lengte geen invloed heeft op de forloop. We kunnen dus eenvoudig kamers toevoegen zonder dat dit invloed heeft op de werking van het programma. We zullen blijven behouden doorheen het hele programma (de speciale kaart uitgezonder in fase 8).

Fase 2: Een interactief saai spel
We bieden de mogelijkheid aan aan de gebruiker om zelf te kiezen naar welke kamer er wordt gegaan. De gebruiker kan dus "vooruit’" of "achteruit" gaan in de array. We houden hiervoor een variabele (huidigekamer) bij die bijhoudt waar de gebruiker zich momenteel bevindt.
Telkens de gebruiker zich wil verplaatsen controleren we of deze verplaatsen toegestaan is. De Huidigekamer variabele is dus automatisch ook de index van de te tonen kamer in de string-array.

Fase 3: Een 2D-wereld met lookup-table
Stap 1: Kaart maken
Vervolgens willen we de mogelijkheid om een 2D wereld aan te bieden. Hierbij gebruiken we een zogenaamde lookup-table zodat we onze wereld array eenvoudig kunnen houden én kamers kunnen herbruiken.
Eerste definiëren we de verschillende kamers die er bestaan:
Vervolgens maken we 2D-array die onze kaart voorstelt. De array is van het type int. Iedere cijfer in de array zal de index bevatten van de kamer die op die plek moet komen. Dit is dus een zogenaamde look-up-table of Lut (meer info: wiki):
Linksboven beginnen we dus met een Gang, met rechts ervan een lobby, etc.
Plaatsen die we met een 0 (onbekend terrein) definiëren gaan we beschouwen als plaatsen waar de gebruiker niet mag komen.
Merk op dat we dus onze wereld zo groot of zo klein kunnen maken als we zelf wensen.
Stap 2: Wandelen op de kaart
Daar we ons nu op een 2D-kaart bevinden hebben we 2 variabelen nodig om onze huidige positie te onthouden:
We spreken af dat de locatie (0,0) zicht linksboven in de array bevindt.
We maken een oneindige loop die steeds de volgende stappen zal doen:
Huidige kamertekst op het scherm tonen
Aan de gebruiker vragen naar waar hij wil wandelen
Positie van gebruiker veranderen
Terug naar 1.
Eerst gebruiken we dus de lut om de huidige kamer beschrijving te tonen. We gebruiker de huidige spelerlocatie als index’s in de Kaart-array en vragen zo de kamerindex op. Die kamerindex gebruiken we om de tekst uit de Kamers-array te tonen.
De gebruiker kan zich naar het noorden, oosten, zuiden of westen begeven (respectievelijk naar boven, links, onder, rechts op de kaart). We vragen dus telkens de gebruiker naar waar hij:
Stap 3: Positie aanpassen
We verwerken de richting in een switch:
Naargelang de richting die de gebruiker ingeeft moeten we dus telkens 2 zaken contoleren:
Bevindt de gebruiker zich momenteel (VOOR we z’n locatie aanpassen) aan de rand van de array (0 of Length-1)
Probeert de gebruiker zich naar verboden vakje te begeven (een onbekend terrein vakje)
Indien aan deze 2 voorwaarden niet is voldaan dan mogen we de huidige locatie van de gebruiker zonder problemen veranderen. Dit behelst dus dat we , naargelang de richting, de posX en posY waarden veranderen, namelijk:
Noorden: posX met 1 verlagen
Zuiden: posX met 1 verhogen
Oosten: posY met 1 verhogen
Westen: posY met 1 verlagen
We krijgen in de switch dus:
Stap 4: Volledige code
De volledige code van deze fase is dus geworden:
Fase 4: Tekenen van de kaart op het scherm
We wensen een visuele indicatie van de kaart te tonen aan de gebruiker (zonder dat hij ziet wat voor kamer het is). We voegen daarom een methode DrawMap() toe die de kaart iedere keer opnieuw zal tekenen. Deze methode gaat ook de positie van de gebruiker duidelijk maken a.d.h.v. een "X" op de kaart. Onze game-loop veranderen we dus naar:
De DrawMap() methode toont dus de huidige locatie als een "X". Voorts willen we dat enkel bereikbare kamers getoond worden (we gebruiken een "O" hiervoor). Elementen op de kaart die wijzen naar index 0 ("Onbekend terrein") worden niet getoond.
We doorlopen in de DrawMap() methode de volledige kaart. Lijn per lijn. Hiervoor gebruiken we 2 geneste for-loops. De outer-loop (index i) zal de X-coördinaat aflopen, oftewel lijn per lijn. De inner loop (index j) zal de Y-coördinaat aflopen, oftewel kolom per kolom:
Merk op dat ook deze methode geen hardcoded array-grenzen bevat. We kunnen dus eender welke kaart aan deze methode aanbieden.
Binnen de inner-for gaan we nu element per element van 1 rij op het scherm tonen. Eerst controleren we of de speler zich bevindt in het element dat we op het punt staan te tekenen. Als dat zo is dan plaatsen we een "X":
Anders plaatsen we een "o" indien het gaat om gebied waar de speler toegelaten is:
Niet toegelaten gebied tonen we niet, we zetten dus een spatie in de plaats:
Na iedere inner-loop moeten we vervolgens een newline toevoegen, anders worden alle rijen van de kaart naast elkaar gezet. Finaal krijgen we dus:
Dit resulteert in volgende finale code voor deze fase:
Fase 5: Kaart vergroten
Zoals reeds aangehaald staat niets je in de weg om je spel-wereld groter te maken. Hiervoor hoef je enkel (momenteel) de Kamers en Kaart arrays aan te passen. Alle code zal blijven werken.
Bijvoorbeeld:

Fase 6: Een extra look-up-table voor meer wereld-details
Het principe van een lut verschilt eigenlijk weinig van een eenvoudige database. We zouden dus meerdere look-up-tables (tabellen) kunnen definiëren en deze gebruiken om meer informatie in onze spelwereld te plaatsen.
We kunnen bijvoorbeeld per kamer ook een beschrijving tonen van die kamer. Daar we nog niets kennen van struct en class (zogenaamde datastructuren) moeten we ons dus behelpen als volgt: we definiëren een nieuwe array Beschrijving waarbij ieder element de index heeft van de respectievelijke kamer:
Door 1 extra lijntje (+ eentje voor een visuele scheiding tussen beschrijving en kamertitel) plaatsen we nu steeds de kamerbeschrijving onder de kamertype:

Fase 7: Dynamische kaart
Na iedere actie van de speler verwerken we steeds weer de kaart in zowel de DrawMap()-methode als tijdens het verwerken van de speler-input. We kunnen dus eenvoudig een dynamische kaart maken die zich aanpast naargelang bepaalde acties.
Je met de kennis die we zo meteen tonen bijvoorbeeld aan de start van het programma met een lege kaart: naargelang de speler zich verplaatst in de wereld zal de kaart aangevuld worden. (tip: gebruik hiervoor een array VolledigeKaart en een array ReedsOntdekteKaart of iets dergelijks. De speler krijgt steeds de ReedsOntdekteKaart te zien in DrawMap(). Naargelang acties van de speler kopieer je dan bepaalde elementen uit VolledigeKaart naar ReedsOntdekteKaart).
We definiëren onze kaart (merk op dat we de folterkamer en geheime gang verwijderen rechts onderaan):
We willen volgende functionaliteit inbouwen:
indien de gebruiker in de kamer met index 6 ("SecurityRoom") een bepaalde actie onderneemt dan zal een geheime gang en kamer (folterkamer) op de kaart bij verschijnen, rechts van de securityroom.
De actie gaan we nu even eenvoudig beschouwen als volgt: de gebruiker kan in alle kamers "G" als opdracht doorgeven. Echter, enkel wanneer de gebruiker zich in de kamer met index 6 bevind dan zal de geheime kamer zichtbaar worden. We voegen daarom een extra case toe aan onze switch:
Als de speler wél in de securityroom is dan gaan we de kaart-array aanpassen. We voegen rechtsonder in de array de 2 nieuwe kamers toe:
Wanneer we nu de kaart hertekenen dan deze nieuwe ruimte verschijnen en weet de gebruiker dat hij zich daar kan begeven.
De volledige code wordt dan (we laten de DrawMap()-methode even achterwege):


Fase 8: Go nuts
Vanaf dit punt kun je nu al een relatief eenvoudig, toch leuk spel maken, op voorwaarde dat je verhaal goed zit. Echter, voor we je hierop loslaten gaan we nog enkele zaken refactoren zodat de code wat leesbaarder blijft. In hoofzaak willen we bepaalde stukken code uit de main-body halen en naar aparte methodes extraheren.
Stap 1: Kaart initialiseren in aparte methode
Beeld je in dat je de kaart(en) voor je spel uit een bestand laadt. Op zich is dat niet zo moeilijk , maar het vereist natuurlijk extra lijnen code in je, reeds overbevolkte, Main-methode. We verhuizen daarom de code waarin we onze kaarten initialiseren naar een aparte methode. In onze Main schrijven we dan (merk het gebruik van het out keyword op!):
Deze methode bevat dan gewoon de code van daarnet, mooi verpakt en afgeschermd:
Stap 2: Input verwerken in aparte methode
Het verwerken van de userinput kunnen we ook makkelijk extraheren naar aparte methode zodat onze while-loop overzichtelijker wordt :
Merk op dat we zelfs de volledige loop naar een aparte methode op zijn beurt kunnen extraheren. Maar dat laten we aan de lezer over. We dienen de posities van de speler by reference mee te geven, daar we de posities onmiddellijk willen updaten in de VerwerkInput()-methode.
Stap 3: Een mooier kaartje tekenen
Als kers op de taart tonen we snel hoe je het kaartje sexier kan tonen op het scherm. Hier zijn echter een paar belangrijke opmerkingen aan de orde:
De code bevat enkele hardcoded waarden zoals het plaatsen van de cursor m.b.v.
Console.SetCursorPosition. Beter zou zijn als deze waarden als Magic numbers worden behandeld of on-the-fly worden berekend.De kaart bevat Unicode-art met vaste grootte. Dit zal bugs geven indien onze kaart-array groter is dan de dimensies van de Unicode-art: de art zal over de randen van de Unicode-art getekend worden. We kunnen dit oplossen door delen van de Unicode-art te berekenen (bv het aantal lege lijnen en de breedte van een pagina.
We definiëren de nieuwe Methode en voegen als eerste actie Unicode-art toe van een kaart:
Daar we gaan spelen met de kleuren is het aan te raden om steeds volgende acties te ondernemen indien we de kleur van een bepaald karakter of zinnen willen veranderen:
De huidige kleur van de console bewaren (fore en/of background) in een tijdelijke variabele
Kleur veranderen
Karakter of zin op scherm plaatsen
Kleur terug naar de huidige kleur aanpassen. We tonen dit in de volgende code waarin we de background array (die de Unicode-art bevat) op het scherm willen tekenen. Daarbij willen we dat de karakters donker-cyaan zijn en dat enkel karakters die geen spatie of liggenstreepje zijn een donkergele achtergrond hebben. De commentaar toont de zonet beschreven stappen:
Vervolgens gebruiken we SetCursorPosition om onze spelerskaart ‘over’ de Unicode-art te tekenen. Hierbij voegen we nog wat extra kleurtje toe, de speler-X wordt rood gekleurd:
De while-loop in de Main()-methode passen we nu nog aan zodat:
We de nieuwe DrawCoolMap methode gebruiken
De titel van de kamer steeds op de rechterpagina van de kaart Unicode-art wordt getoond
De beschrijving en andere tekst steeds onder map komt en niet erover
Alle code samen
We zijn er!
De volledige code voor dit extra-ordinaire spel wordt dan:
Main-methode
InitialiseerSpel-methode
VerwerkInput-methode
DrawMapCool-methode
Last updated
Was this helpful?