Gå till innehållet

Projektuppgift 1 - 21:an

Introduktion till projektuppgift 1

Detta är den första av bokens projektuppgifter där du kommer få träna på att skriva lite större program än de som du gör som uppgifter i övriga kapitel. Just den uppgift går ut på att göra en variant av kortspelet 21, också kallat Black Jack. Vi kommer att kalla vårt spel för 21:an.

Innan själva projektuppgiften börjar kommer du att få se ett exempel som visar hur man gör för att generera slumptal eftersom detta kommer krävas i 21:an. Därefter kommer vi steg för steg att planera vad vi ska programmera härnäst och därefter skriva koden som vi behöver. Avslutningsvis så kommer du att hitta förslag på vad du själv kan göra för att bygga vidare 21:an för att göra spelet ännu roligare.

I 21:an kommer du att spela mot datorn och försöka tvinga datorn att få över 21 poäng. Både du och datorn får poäng genom att dra kort, varje kort är värt 1 – 10 poäng. När spelet börjar dras två kort till både dig och datorn. Därefter får du dra hur många kort som du vill tills du är nöjd med din totalpoäng, du vill komma så nära 21 som möjligt utan att få mer än 21 poäng. När du inte vill dra fler kort så kommer datorn att dra kort tills den har mer eller lika många poäng som dig.

Du vinner om datorn får mer än totalt 21 poäng när den håller på att dra kort. Datorn vinner om den har mer poäng än dig när spelet är slut så länge som datorn inte har mer än 21 poäng. Om det skulle bli lika i poäng så vinner datorn. Om du får mer än 21 poäng när du drar kort så har du förlorat.

Vi kommer att bygga 21:an som ett menyprogram så att användaren får möjlighet att spela så många gånger hen vill utan att behöva starta om programmet. Innan vi sätter igång och kollar på hur man kan generera slumptal så kan du här se en körning av programmet där användaren spelar 21:an en gång och vinner mot datorn.

Exempelkörning av 21:an

Välkommen till 21:an!
Välj ett alternativ
1. Spela 21:an
2. Visa senaste vinnaren
3. Spelets regler
4. Avsluta programmet
1

Nu kommer två kort dras per spelare
Din poäng: 4
Datorns poäng: 15
Vill du ha ett till kort? (j/n)
J
Ditt nya kort är värt 8 poäng
Din totalpoäng är 12
Din poäng: 12
Datorns poäng: 15
Vill du ha ett till kort? (j/n)
j
Ditt nya kort är värt 4 poäng
Din totalpoäng är 16
Din poäng: 16
Datorns poäng: 15
Vill du ha ett till kort? (j/n)
j
Ditt nya kort är värt 4 poäng
Din totalpoäng är 20
Din poäng: 20
Datorns poäng: 15
Vill du ha ett till kort? (j/n)
n
Datorn drog ett kort värt 8
Din poäng: 20
Datorns poäng: 23
Du har vunnit!
Skriv in ditt namn
Simon

Välj ett alternativ
1. Spela 21:an
2. Visa senaste vinnaren
3. Spelets regler
4. Avsluta programmet

Detta var alltså en körning där användaren lyckades besegra datorn eftersom datorn fick mer än 21 poäng. Innan vi börjar planera hur vi ska programmera 21:an så ska vi se hur man kan generera slumptal.

Slumptal med Random-variabler

När man vill generera slumptal så gör man det med en variabeltyp som vi inte har sett tidigare, denna variabeltyp kallas för Random. Man kan döpa variabeln till vad man vill men eftersom den fungerar som en slumptalsgenerator så är det detta vi kommer att kalla den i detta exempel. En Random-variabel skapas med hjälp av ordet new.

// En Random-variabel används för att slumpa fram tal
Random slumptalsgenerator = new Random();

// Ett slumptal från 0 till 4
int slumptal1 = slumptalsgenerator.Next(5);

// Ett till slumptal från 0 till 4
int slumptal2 = slumptalsgenerator.Next(5);

// Ett slumptal från 10 till 19
int slumptal3 = slumptalsgenerator.Next(10, 20);

// Ett slumptal från -10 till 2
int slumptal4 = slumptalsgenerator.Next(-10, 3);

// Ett slumptal som är större än eller lika med 0.0 och mindre än 1.0
double slumptal5 = slumptalsgenerator.NextDouble();

Console.WriteLine($"Tal 1: {slumptal1}");
Console.WriteLine($"Tal 2: {slumptal2}");
Console.WriteLine($"Tal 3: {slumptal3}");
Console.WriteLine($"Tal 4: {slumptal4}");
Console.WriteLine($"Tal 5: {slumptal5}");

Console.ReadKey();

I exemplet så används några av de metoder som man kan använda med en Random-variabel. Den som används först är metoden Next där man får ange ett tal när man anropas metoden. Programmet kommer då att generera ett slumpat heltal som är från 0 till upp till ett mindre än det tal man skriver in, i det första fallet ett heltal från 0 till 4.

När man anger två tal när man anropar metoden Next så kommer ett heltal genereras som är större än eller lika med det första talet och mindre än det andra talet. I det första exemplet som anropar Next med två tal fås därför ett heltal från 10 till 19.

Det går också att generera slumpade decimaltal med hjälp av en Random-variabel, vi kommer dock inte att använda det när vi ska programmera spelet 21:an. När man anropas metoden NextDouble får man en double som är större än eller lika med 0 och mindre än 1. Vill man ha ett decimaltal som är slumpat inom t.ex. intervallet 0 till 5 så får man själv multiplicera talet som man får från NextDouble med 5.

Endast en Random-variabel per program

Använd endast en Random-variabel per program, två Random-variabler som skapas samtidigt genererar samma slumptal.

Slutligen kan du notera att alla slumpade tal i detta exempel genererades med samma Random-variabel. Som regel bör du aldrig skapa mer än en Random-variabel i dina program oavsett hur många slumptal du vill skapa. Anledningen till detta är att två Random-variabler som skapas samtidigt kommer att generera samma slumptal vilket kan leda till resultat som inte verkar särskilt slumpartade.

Uppgift P1.1

Skapa ett program som ska fungera som ett lyckohjul. När användaren kör programmet får hen välja ett tal från 1 till 10 som hen tror att lyckohjulet kommer att stanna på. Slumpa därefter fram ett tal från 1 till 10 och säg till användaren vilket tal det blev. Berätta också för användaren om hen vann eller förlorade.

Lösningstips P1.1

Tänk på att använda slump.Next(1, 11) för att slumpa ett tal mellan 1 och 10.

Lösningsförslag P1.1

Uppgift P1.2

Skapa ett program som ska fungera som en tärningssimulator. När användaren startar programmet ska hen få säga hur många tärningar hen vill kasta och hur många sidor varje tärning ska ha, en tärning måste ju inte ha 6 sidor även om detta är vanligast. Simulera därefter det antal tärningskast som användaren vill göra med det antal sidor som användaren valde. Skriv ut värdet av varje tärningskast.

Lösningsförslag P1.2

Planering och implementering av spelet

Vi ska börja med att planera vårt program. Vi kommer göra detta med hjälp av pseudokod. Pseudo betyder ”falsk” och pseudokod är text som ser ut som kod men som inte är riktigt kod, det går inte att köra ett program som består av pseudokod. Det blir helt enkelt en beskrivning av vårt program med text som är en blandning av svenska och kod. Det går så klart att skriva pseudokoden med papper och penna eller med valfritt ordbehandlingsprogram, men vi kommer att skriva vår pseudokod som kommentarer i vårt program. En fördel med detta är att vi kan ha ta bort kommenterarerna allteftersom vi skriver mer kod i vårt program och faktiskt programmerar det som vår planering anger. Vi kan också välja att låta en del kommentarer stå kvar som dokumentation av vårt program. Vi behöver inte planera hela programmet innan vi sätter igång och programmerar, först så gör vi en planering av programmets huvudmeny och vad som ska hända beroende på vad användaren väljer för alternativ. Huvudmenyn för programmet ska se ut så här

Välj ett alternativ
1. Spela 21:an
2. Visa senaste vinnaren
3. Spelets regler
4. Avsluta programmet
Vi börjar nu med planeringen av huvudmenyn.

// Skriv välkomstmeddelande

// Sätt menyVal till "0"

// while (menyval != ”4”)

    // Skriv ut meny

    // Läs in menyVal

    // switch menyVal
        // case 1: Spela en omgång av 21:an
        // case 2: Visa senaste vinnaren
        // case 3: Visa spelets regler
        // case 4: Gör ingenting (programmet avslutas)
        // default: Skriv att alternativet var ogiltigt

Än så länge är programmet som ett vanligt menyprogram som inte gör någonting. Vi fyller i den kod som behövs för att få ett fungerande menyprogram enligt planeringen. Att skriva kod som uppfyller vår planering kallas för att vi implementerar planeringen.

Console.WriteLine("Välkommen till 21:an!");

string menyVal = "0";
while (menyVal != "4")
{
    // Skriv ut huvudmenyn
    Console.WriteLine("Välj ett alternativ");
    Console.WriteLine("1. Spela 21:an");
    Console.WriteLine("2. Visa senaste vinnaren");
    Console.WriteLine("3. Spelets regler");
    Console.WriteLine("4. Avsluta programmet");
    menyVal = Console.ReadLine();

    // Tom rad innan användarens val körs
    Console.WriteLine();

    switch (menyVal)
    {
        // Spela en omgång av 21:an
        case "1":
            break;

        // Visa senaste vinnaren
        case "2":
            break;

        // Visa spelets regler
        case "3":
            break;

        // Gör ingenting (programmet avslutas)
        case "4":
            break;

        default:
            Console.WriteLine("Du valde inte ett giltigt alternativ");
            break;
    }

    // Tom rad innan nästa körning
    Console.WriteLine();
}

Vi har nu ett program som innehåller en fungerande huvudmeny som inte gör någonting förutom att skriva ut lite text om användaren skriver in ett ogiltigt alternativ. Det är trots det ett fungerande skal som vi kan bygga vidare från och implementera resten av programmet bit för bit.

Behålla kommentarerna med pseudokod

Som du ser i vårt exempel så kan det ibland vara en bra idé att behålla några av pseudokodkommentarerna som man skrev när man planerade ett program. Dessa kommentarer fungerar bra som dokumentation, d.v.s. beskrivning av hur programmet är kodat.

Av de tre menyval som vi behöver skriva kod för så är menyval 1 det som kommer att ta längst tid eftersom vi där ska skriva all kod för själva spelet. Menyval 2 behöver bara innehålla en enda Console.WriteLine som skriver ut namnet på den som senast har vunnit spelet och menyval 3 innehåller också bara några androp till Console.WriteLine. Vi skriver koden för dessa två menyval först så har vi endast huvuddelen av programmet, menyval 1, kvar att skriva efteråt.

Eftersom vi vill spara namnet på den person som vann spelet senast så måste vi ha en variabel för den senaste vinnaren. Denna variabel måste bevara sitt värde mellan olika körningar av while-loopen som innehåller huvudmenyn och därför måste den deklareras innan denna loop börjar. Vi lägger därför till följande kod längst upp i programmet.

Random slump = new Random();            
string senasteVinnaren = "Ingen har vunnit än";      

Vi passar på att göra en Random-variabel här också eftersom vi kommer att behöva generera slumptal i menyval 1. Vi väljer att kalla variabeln slump istället för slumpgenerator för att minska på antalet tecken som skrivs varje gång vi ska generera slumptal. Nu när vi har en variabel som ska innehålla namnet på den senaste vinnaren kan vi lägga till koden som ska vara för menyval 2.

// Visa senaste vinnaren
case "2":
    Console.WriteLine($"Senaste vinnaren: {senasteVinnaren}");
    break;

Koden för menyval 3 behöver inga variabler eller någonting annat för att fungera, det är bara några som skriver ut spelets regler. Notera en del av koden som visas här är för bred för att få plats i boken och därför delvis sträcker sig över flera rader här även fast den inte gör det i koden.

// Visa spelets regler
case "3":
    Console.WriteLine("Ditt mål är att tvinga datorn att få mer än 21 poäng.");
    Console.WriteLine("Du får poäng genom att dra kort, varje kort har 1-10 poäng.");
    Console.WriteLine("Om du får mer än 21 poäng har du förlorat.");
    Console.WriteLine("Både du och datorn får två kort i början. Därefter får du");
    Console.WriteLine("dra fler kort tills du är nöjd eller får över 21.");
    Console.WriteLine("När du är färdig drar datorn kort till den har");
    Console.WriteLine("mer poäng än dig eller över 21 poäng.");
    break; 

Nu är det dags att ta itu med menyval 1, själva spelet. Eftersom denna del av programmet är mer komplex än menyval 2 och 3 så ska vi först planera den med pseudokod och sedan implementera den. Eftersom det kommer bli en hel del kod så gör vi planeringen och implementeringen i flera steg. Vi börjar med att göra en planering där två kort dras för både datorn och spelaren, därefter får spelaren dra kort tills hen är nöjd eller får mer än 21 poäng. Vad vi kommer ha kvar att planera senare är att låta datorn dra kort och undersöka vem som vann spelet.

// Spela en omgång av 21:an
case "1":
    // Sätt spelarensPoäng och datornsPoäng till 0
    // Dra två kort per spelare

    // sätt kortVal till "j"
    // while (kortVal != "n" och spelarensPoäng <= 21)
        // Skriv ut poängställningen
        // Fråga om spelaren vill ha ett till kort
        // Läs in kortval

        // switch kortVal
            // case "j": Dra ett kort till
            // case "n": Gör ingenting (loopen kommer sluta köras)
            // default: Skriv att alternativet var ogiltigt
    break;

Med denna del av planeringen färdigskriven så kan vi gå direkt till implementeringen. Vi drar nya kort genom att slumpa ett tal från 1 till 10 och lägga till kortets poäng till spelarens eller datorns totalpoäng.

// Spela en omgång av 21:an
case "1":
    int datornsPoäng = 0;
    int spelarensPoäng = 0;
    Console.WriteLine("Nu kommer två kort dras per spelare");
    datornsPoäng += slump.Next(1, 11);
    datornsPoäng += slump.Next(1, 11);
    spelarensPoäng += slump.Next(1, 11);
    spelarensPoäng += slump.Next(1, 11);

    // Låt användaren dra fler kort
    string kortVal = "";
    while (kortVal != "n" && spelarensPoäng <= 21)
    {
        Console.WriteLine($"Din poäng: {spelarensPoäng}");
        Console.WriteLine($"Datorns poäng: {datornsPoäng}");
        Console.WriteLine("Vill du ha ett till kort? (j/n)");
        kortVal = Console.ReadLine();

        switch (kortVal)
        {
            case "j":
                int nyPoäng = slump.Next(1, 11);
                spelarensPoäng += nyPoäng;
                Console.WriteLine($"Ditt nya kort är värt {nyPoäng} poäng");
                Console.WriteLine($"Din totalpoäng är {spelarensPoäng}");
                break;

            case "n":
                break;

            default:
                Console.WriteLine("Du valde inte ett giltigt alternativ");
                break;
        }

    }

    break;

Med denna kod skriven så kan man köra en första version av spelet. Datorn och spelaren får sina kort när spelet startar och spelaren får därefter dra så många kort hen vill så länge hen inte får mer än 21 poäng.

Vi ska nu planera färdigt det som är kvar att göra i spelet. Denna planering skrivs efter den kod som vi redan har skrivit inuti menyval 1.

// Gå tillbaka till huvudmenyn om användaren har över 21
// Datorn drar kort tills den vinner eller går över 21
// Skriv ut datorns och spelarens poäng
// Undersök vem som vann
// Om spelaren vann så får hen skriva in sitt namn

Vi börjar med att undersöka om spelaren har mer än 21 poäng, då vet vi att hen har förlorat och kan skriva ut det och sedan gå tillbaka till huvudmenyn genom att avbryta det case som vi är i. Därefter ska datorn dra kort tills den vinner eller går över 21 poäng. Detta kommer vi att göra med hjälp av en while-loop, men till skillnad från när vi har planerat loopar tidigare så har vi inte skrivit ut vilka variabler loopen ska undersöka och inte heller hur koden inuti loopen ska se ut.

Pseudokodens utseende och regler

Kom ihåg att pseudokod inte har några egentliga regler för hur den måste se ut och poängen med den är att vi ska få en planering som vi sedan ska följa när vi skriver kod, så du får själv skriva pseudokoden på det sätt som passar dig.

Slutligen så vi undersöka vem som vann spelet. Nu går vi till implementeringen av denna sista del.

// Gå tillbaka till huvudmenyn om användaren har över 21
if (spelarensPoäng > 21)
{
    Console.WriteLine("Du har mer än 21 och har förlorat");
    break;
}

// Datorn drar kort tills den vinner eller går över 21
while (datornsPoäng < spelarensPoäng && datornsPoäng <= 21)
{
    int datornsNyaPoäng = slump.Next(1, 11);
    datornsPoäng += datornsNyaPoäng;
    Console.WriteLine($"Datorn drog ett kort värt {datornsNyaPoäng}");
}

Console.WriteLine($"Din poäng: {spelarensPoäng}");
Console.WriteLine($"Datorns poäng: {datornsPoäng}");

// Undersök vem som vann
if (datornsPoäng > 21)
{
    Console.WriteLine("Du har vunnit!");
    Console.WriteLine("Skriv in ditt namn");
    senasteVinnaren = Console.ReadLine();
}
else
{
    Console.WriteLine("Datorn har vunnit!");
}

Med hjälp av ett break inuti if-satsen som kollar om spelarens poäng är mer än 21 så kan vi lämna det case vi är inne i tidigare än i slutet. Datorn kommer att dra poäng tills den vinner över användaren eller har gått över 21, detta kan vi uttrycka som att den drar kort så länge som användarens poäng är större än datorns poäng och datorns poäng samtidigt är mindre än 21. För att undersöka vem som vann kollar vi bara om datorn har fått mer än 21 poäng eftersom det är det enda sättet som spelaren kan ha vunnit. Om datorn har mindre än 21 poäng så har den det eftersom den slutade dra kort i förtid och det gör den bara om den har mer poäng än spelaren eller om det skulle vara lika, och datorn vinner om det är lika i poängställningen.

Om användaren skulle vinna så får hen skriva in sitt namn som sparas i variabeln senasteVinnaren, detta namn kan sedan visas från programmets huvudmeny.

Hela programmet

Följ länken nedan för att se koden till hela programmet.

Hela programmet

Detta program är det största som du har fått skriva hittills om du enbart har följt bokens exempel och uppgifter. Programmet börjar nå en längd som gör att det blir svårt att få en lätt överblick över hur det fungerar, du kommer i senare kapitel se hur man kan strukturera sin kod för att den ska bli enklare att förstå och då få tips på hur du kan förbättra 21:an.

Förslag på förändringar och utökningar av 21:an

Här följer förslag på vad du själv kan göra för att bygga ut 21:an. Du väljer själv hur mycket du vill göra och vad du har tid med, men du rekommenderas att åtminstone genomföra delen ”Variabler för spelets inställningar”.

Variabler för spelets inställningar

Man ska sträva efter att det ska vara så enkelt som möjligt att göra ändringar av sina program. Om man vill att någon funktionalitet ska förändras ska man helst bara behöva ändra saker på ett ställe i koden om det är möjligt. Som programmet ser ut nu så skulle man behöva ändra många värden i koden om man skulle vilja byta ut hur många poäng som ett kort maximalt kan ge när det dras, just nu är maxpoängen 10 per kort. För att underlätta förändringar och tester av andra maxpoänger så skulle vi behöva byta ut 11:orna som står när poängen slumpas fram mot en variabel som skulle kunna heta maxKortPoäng.

  • Skapa en variabel längst upp i programmets kod som heter maxKortPoäng som styr hur många poäng ett kort maximalt kan ge.
  • Styr den minimala poängen som ett kort kan ge med hjälp av en variabel på samma sätt som det maximala poängen.
  • Kan du komma på vilka mer inställningar som spelet har som skulle kunna styras med variabler istället för tal? Använd variabler så att du enkelt kan ändra programmets inställningar och testa dig fram till en spelvariant som du tycker om.

Programmets utseende och känsla

Ändra programmets titel, bakgrundsfärg och textfärg till någon kombination som du tycker om.

Skriv ut text i olika delar av programmet i olika färg, t.ex. skulle alltid spelarens poäng kunna skrivas i grönt och datorns poäng i rött. Finns det annan information som skulle kunna skrivas i olika färger?

Använd Thread.Sleep för att all text som programmet skriver ut inte ska komma direkt. T.ex. så skulle programmet kunna skriva ut ”...” på en egen rad varje gång datorn ska dra ett kort. Skriv ut punkterna med 0,2 sekunders mellanrum. Kan du hitta andra roliga användningar för Thread.Sleep i spelet?

Skriv ut ett tydligare meddelande när en omgång av 21:an är klar som förklarar varför spelaren eller datorn vann. Datorn kan ju vinna p.g.a. att den hade flest poäng eller för datorn och spelaren hade lika många poäng och datorn vinner oavgjorda matcher, låt användaren veta vad som hände genom att skriva det i text.

Spelets regler och funktion

Gör så att datorn slutar dra kort innan den når 21 för att användaren lättare ska vinna. Låt en variabel styra vilken som är datorns sluta-dra-poäng.

Dra automatiskt kort åt spelaren om datorn har mer poäng än vad spelaren har. Använd Thread.Sleep vid denna automatiska dragning så att det blir mer spännande för spelaren.

Gör så att alla kort inte är lika vanliga. T.ex. så skulle det kunna vara dubbelt så vanligt att man får ett kort med antingen 1 eller 10 poäng jämfört med övriga kort.

Svårighetsgrader

Skapa ett nytt alternativ i huvudmenyn som du kallar för ”Inställningar”. När man väljer detta alternativ ska man komma till en ny meny som påverkar spelets svårighetsgrad. Användaren ska kunna välja mellan tre svårighetsgrader som påverkar vilken poäng som datorn slutar dra kort vid. Användaren ska också kunna välja ifall datorn eller spelaren vinner om de har lika många poäng. Om du vill så kan du också lägga till en inställning som anger hur många kort datorn och spelaren drar i spelets början.

Upprepade körningar

Gör så att man genast får spela en ny omgång av 21:an om man vinner. Man ska fortsätta spela ända till man förlorar. Byt ut variabeln som sparar namnet på den senaste vinnaren mot en variabel som sparar namnet på den spelare som vann flest omgångar av 21:an i rad utan att förlora.

Förslag på egen projektuppgift – Gissa talet

Skapa ett eget spel som du kallar för ”Gissa talet”. I varje omgång av gissa talet ska datorn slumpa fram ett heltal mellan 1 och 10. Användaren ska gissa vilket talet är ända tills hen gissar rätt. Efter varje gissning ska användaren få veta om gissningar var rätt, för hög eller för låg.

Gör programmet till ett menyprogram. Börja med att planera programmet med pseudokod. Implementera därefter programmet och försök att själv komma på utökningar av det, en del av de utökningar som föreslogs till spelet 21:an kan du också genomföra för spelet Gissa talet.