Hej, och välkomna till den sista delen i “Den där gången jag byggde Snake 2”, en serie texter om den där gången jag byggde Snake 2. Läs del ett här, del två här och del 3 här.

Jag hade planerat att göra det här till en kvintologi, med spelets två sista stora funktioner i den här texten följt av en avslutande text som pratade om saker som highscore, cookies och CSS-besvär. Men alla de sakerna låter väldigt tråkiga, så vi struntar i dem!

När vi sist sa adjö hade jag precis ordnat så att ormen faktiskt gick att styra. Med piltangenterna kunde man nu få ormen att åka precis dit man ville, vilket tekniskt sett kvalificerar bygget som ett spel. Men för att faktiskt göra spelet värt att spela behövs mer än bara interaktivitet: det behövs piska och det behövs morot. Och eftersom jag personligen är mer av en morotskille så är det där vi börjar: med maten.

Just maten var faktiskt en av de huvudsakliga anledningarna att jag fick idén att göra Snake från första början. När jag experimenterade lite med att stoppa in och plocka ut saker ur arrayer slog det mig att det skulle funka exceptionellt väl för just mat-funktionen i Snake.

När ormen är i rörelse lägger koden hela tiden till en ny ruta i ormens array, samtidigt som den tar bort rutan längst bak. När man passerar över en bit mat är det bara att placera matbiten också i arrayen, så att rutan där maten låg hamnar i arrayen dubbelt upp. När det sen blir dags för ormen att lämna den rutan så tar det ett steg extra, och ormen har plötsligt blivit lite längre.

Men innan vi kommer till det behöver vi placera ut en matbit.

function makeFood() {
        r = Math.floor(Math.random() * rows) + 1
        c = Math.floor(Math.random() * columns) + 1
        food = "#r" + r + "c" + c;
        $(food).css('background-color', 'black');
}

Wow, fantastisk kod, alldeles magisk. Jag är blåst av stolen.

De två sista raderna i funktionen är saker vi pratat om innan, först kombineras variablerna för rad och kolumn till ett komplett div-ID som sparas i variabeln “food”, och sen byter vi färg på den utvalda rutan till svart. Enkelt.

De två raderna innan dem hanterar randomiseringen, så att vi får en slumpvis utvald ruta att göra till mat. Det gör det med hjälp av “Math” som, föga förvånande, låter oss göra matte. Math är vad som kallas för ett objekt i javascript, men det spelar ingen roll, så skit i det. Vad som är viktigt är att Math innehåller en massa funktioner som låter oss göra saker med siffror. Och för att placera ut maten använder jag två av de funktionerna.

Den första funktionen är Math.random(), som genererar ett slumpmässigt tal som är större eller lika med 0 och mindre än 1. Det talet multiplicerar jag sen med det totala antalet rader för att få ut en slumpmässig rad och det totala antalet kolumner för att få ut en slumpmässig kolumn.

Eftersom Math.random() genererar en massa decimaler jag inte vill ha så använder jag sen en annan Math-funktion, Math.floor(), som helt enkelt rundar siffran jag får ut till närmsta heltal. Och eftersom Math.random() kan generera 0 men inte 1 så kan resultatet ibland bli 0, men aldrig bli exakt samma som antalet rader eller kolumner. Därför lägger jag helt enkelt på en etta på slutet. Matte!

Så vi har skapat en bit mat, men om man åker över den med ormen så händer ingenting förutom att maten försvinner och ingen ny matbit dyker upp. Nästa steg är att se till att ormen faktiskt växer när den äter.

        if (next == food) {
                snake.push(food)
                makeFood()
        }

Precis som jag beskrev tidigare, väldigt enkelt. Om nästa ruta som ormen åker till är rutan med mat så läggs den till i arrayen “snake”, vilket innebär att ormen blir en plupp längre, och sen genereras en ny bit mat.

I den versionen av koden jag precis skrivit fanns dock ett möjligt problem, och jag var faktiskt väldigt stolt över att jag listade ut det innan jag ens hade stött på problemet. Om jag bara placerar ut matbiten slumpmässigt på planen så ökar risken kontinuerligt för att matbiten ska genereras inuti ormen. När ormen sen rör sig förbi matbiten kommer rutan bli vit och matbiten kommer vara osynlig, men fortfarande inte uppäten. Det här insåg jag alldeles på egen hand och la direkt in en lösning.

Som dock inte fungerade.

När jag publicerade min första text var det ganska direkt någon på twitter som påpekade att de plötsligt inte hade fått någon ny matbit när de ätit upp den förra. Men eftersom jag tyckte att jag redan hade löst det problemet, och eftersom det är ganska meckigt att återskapa utan att spela en massa, så beslutade jag att det säkert var något skumt edge case som i princip aldrig skulle dyka upp, och bara sket i det.

Tills för häromveckan när jag själv stötte på problemet och insåg att det nog faktiskt var så att matbiten genererades inuti ormen. Fortfarande kändes det som ett väldigt jobbigt problem att felsöka, så jag funderade väldigt starkt på att fortsätta ignorera det, men som tur var höll jag precis vid den tiden på med ett annat projekt där jag stötte på en bugg som smällde mig i ansiktet och sa ”det här är vad du har gjort fel, dummerjöns”. Så låt oss titta på min första lösning och sen på den korrekta versionen.

function makeFood() {
        while (food === undefined || snake.includes(food)) {
                r = Math.floor(Math.random() * rows) + 1
                c = Math.floor(Math.random() * columns) + 1
                food = "#r" + r + "c" + c;
        }
        $(food).css('background-color', 'black');
}

Koden jag skrev var baserad på en någorlunda god idé. Jag flyttade in koden som skapade den slumpmässiga matbiten i en while-loop, som kollar ifall variabeln “food” antingen är odefinierad, det vill säga inte har något värde, eller befinner sig inom ormen, och sen körs loopen tills det inte är fallet längre. Så om matbiten genereras inuti ormen så försöker koden bara igen och igen. Därefter ändras färgen på den matbiten till svart.

Men häri ligger problemet. För den koden körs inte alls därefter, den körs så fort loopen har försökt en gång. Så om matbiten hamnar inuti ormen så kommer loopen försöka igen, men koden som ändrar färg på matbiten använder det första resultatet. Så, i den allra senaste versionen av koden ser det istället ut så här:

async function makeFood() {
        await placeFood()
        $(food).css('background-color', color);
}

function placeFood() {
        while (food === undefined || snake.includes(food)) {
                r = Math.floor(Math.random() * rows) + 1
                c = Math.floor(Math.random() * columns) + 1
                food = "#r" + r + "c" + c;
        }
}

Jag har nu flyttat ut koden som väljer matens position till en egen funktion och gjort den första funktionen till en asynkron funktion, så att jag kan säga åt den att vänta tills den nya funktionen faktiskt är klar med vad den gör. Nu skulle man ju kunna tro att det skulle kunna innebära en fördröjning innan maten dyker upp, men allt det här går så förbannat fort att det inte spelar någon roll.

Så här i efterhand slår det mig att jag möjligtvis kunde ha gjort det här mycket lättare genom att bara flytta in “$(food).css(‘background-color’, color);” också i loopen, så att varje bit som väljs byter färg till svart, eftersom bitarna som väljs om är inuti ormen, så ändå redan är svarta. Men av någon anledning känns det mindre korrekt, och jag tror tekniskt sett att det skulle kunna skapa dubbla matbitar i vissa specialfall. Det är vad jag kommer säga till mig själv i alla fall.

Med mat på banan var det så dags för den andra delen av spelarmotivationen: döden. För att spelet ska vara spännande måste man ju ha lite risk. Och om ni tror att det enda som behövs för det är en if-sats som kollar ifall nästa ruta som ormen ska åka in i redan är en del av orm-arrayen, då har ni alldeles rätt. Det var väldigt väldigt enkelt.

        if (snake.includes(next)) {
                alert("You died!")
                location.reload();
        }

I if-satsen sa jag åt sidan att skicka upp en alert-ruta som meddelade spelaren att hen var död, för att sedan ladda om sidan, vilket dock kom med ett litet problem: sidan laddas inte när alert-rutan är uppe, det tar lite tid för sidan att ladda om, och ormen håller fortfarande på att banka huvudet i sin egen röv.

Resultatet blev att man behövde klicka bort rutan om och om igen i väntan på att sidan skulle laddas om, vilket inte var supersnyggt. Det här felet fick dock hänga med tills flera versioner senare, när jag hade introducerat en paus-funktion i spelet. Då var det väldigt enkelt att bara pausa spelet även när ormen dog, så att den slutade ligga och sprattla.

Med det är de stora funktionerna klara och kvar fanns bara quality of life-fixar. I samma version som jag introducerade döden krympte jag också spelplanen till en mer rimlig storlek, och fick till slut spelet att fungera i Firefox, i alla fall tills jag några versioner senare hade sönder det igen och behövde lösa det för en andra gång*.

Att få de grundläggande mekanikerna att fungera var egentligen mitt enda intresse när jag började pilla med Snake, så med introduktionen av döden var jag egentligen klar. Men nu började jag släppa in Svampadeln till att testa spelet och då dök det genast upp nya saker som jag ville fixa.

Spelet fick ett ansiktslyft, med bortplockat rutnät och en funktion för att byta färg på ormen. Jag la även in en poängräknare, och efter att ha muttrat för mig själv i en dag eller så även en highscore-räknare som sparade tidigare resultat som en cookie. Cookies är jobbiga och jag ogillar att hålla på med dem, men jag kunde i alla fall återanvända delar av koden från mitt clicker-spel, så att jag inte behövde anstränga mig så mycket.

Slutligen la jag också in en funktion som anpassa spelets storlek efter skärmen du spelar på, en funktion som helt ärligt funkar sådär. Allting som har att göra med skärmstorlek och att anpassa hur saker ska se ut är det absolut värsta jag vet. CSS är, som alla säkert vet, Guds straff till mänskligheten för våra myriarder synder.

Som sagt i början av den här texten så är alla de här sista uppdateringarna ärligt talat ganska tråkiga, så jag snabbspolar igenom dem här så att jag har nämnt dem. Mitt bootleg-Snake är nu officiellt färdigt, och det är dags för mig att röra mig vidare mot nya projekt. Typ som ett bootleg-Tetris. Något som presenterar en hel rad nya problem, som jag hittills haft varierad lyckad med att lösa. Vi får se om det dyker upp något här på riket om det framöver!

Bing bang bom, slut på text. Det var allt jag hade att säga. Stick och brinn.

*När jag nu försöker återskapa felet i Firefox verkar alla versioner plötsligt och oförklarligt fungera. Den enda förklaring jag kan komma på är att Firefox uppdaterat något som gör att det nu fungerar annorlunda. Vilket väl är bra, antar jag. Hade varit skönt om de kunde ha fixat det lite tidigare bara.