Gå till innehållet

Projektuppgift 3 - Sänka skepp

Introduktion till projektuppgift 3

Denna projektuppgift består av att skapa det klassiska spelet ”Sänka skepp”. I Sänka skepp så har varje spelare en egen karta som man placerar ut skepp på, man får inte se var motståndarens skepp är placerade. Man turas därefter om med försöka skjuta på motståndarens skepp genom att ange vilka koordinater man skjuter på. Till skillnad från föregående projektuppgifter så kräver inte Sänka skepp några nya programmeringskunskaper, vi kan sätta igång direkt med att skapa spelet. Vi kommer inte heller att skapa Sänka skepp som ett menyprogram, det är något som du själv för försöka göra som en utökning av programmet.

Exempelkörning av Sänka skepp

Nedan så visas en exempelvisning av när vår färdiga version av Sänka skepp håller på att spelas mellan spelaren och datorn. I detta exempel så har varje spelare en karta som är 6 rutor bred och 4 rutor hög. Varje ruta på kartorna består av antingen ett O som innebär att rutan är tom eller ett X som innebär att ett skepp finns på denna ruta.

Spelarens karta visas i sin helhet och de rutor som datorn har skjutit på är röda, datorn har alltså 4 missar och 1 träff. Datorns karta visar endast de rutor som användaren har skjutit på, resten visas med ett – per ruta. Spelaren har också 4 missar och 1 träff.

Spelarens karta
OOXOOO
OOOOOO
OOOOOO
OXOOOO

Datorns karta
-O----
---O--
----O-
-XO---
Var vill du skjuta? (X)
6
Var vill du skjuta? (Y)
1

Spelaren är just i färd med att skjuta ett nytt skott på rutan med X-koordinaten 6 och Y-koordinaten 1, d.v.s. rutan längst upp till höger på datorns karta.

Planering och implementering av Sänka skepp

Vi börjar direkt med att göra en grovplanering av hur programmet behöver se ut.

// Skapa kartorna
// Placera skepp på kartorna
// Spela själva spelet

Det som menas med att skapa kartorna är att vi skapar en tvådimensionell array för spelarens karta och en för datorns karta, därefter så fyller vi hela kartorna med tomma platser, alltså bokstaven O. Efter att vi har fyllt hela kartorna med O placerar vi skepp på kartorna genom att byta ut några av de tomma platserna mot X. När kartorna är skapade så kan vi spela själva spelet vilket vi kommer att planera mer i detalj senare.

Var och en av de tre saker som vi har skrivit i vår planering ska skötas av en egen metod som vi sedan kommer att anropa från programmets Main-metod. Anledningen till att vi inte placerar skeppen på kartorna i samma metod som vi skapar kartorna i är att koden som ska placera ut skepp på kartorna kan bli ganska lång och det är bra för programmet struktur och läsbarhet att inte få för långa metoder.

Först av allt så behöver vi de tvådimensionella arrayer som ska innehålla spelarens och datorns kartor. Vi skapar dessa med hjälp av en variabel som anger hur bred kartan är och en som anger dess höjd. Vi passar också på att skapa en Random-variabel eftersom vi kommer att slumpa tal senare i programmet när vi ska välja vilka koordinater som datorn ska skjuta på. Dessa variabler skapas allihopa som klassvariabler, alltså utanför någon metod och vi placerar dem längst upp ovanför Main-metoden.

static int kartBredd = 6;
static int kartHöjd = 4;
static string[,] spelarensKarta = new string[kartBredd, kartHöjd];
static string[,] datornsKarta = new string[kartBredd, kartHöjd];
static Random slump = new Random();

Längden av tvådimensionella arrayer

Det är möjligt att ta reda på längden av en tvådimensionell array på ett liknande sätt som man gör med Length med endimensionella arrayer, men vi har inte gått igenom det. Vi kommer därför att använda oss av variablerna kartBredd och kartHöjd istället för Length i detta program.

Kartorna är nu skapade men de har inget innehåll, vi ska fylla båda kartorna med O på varje plats med hjälp av en nästlad for-loop i en metod som vi kallar för SkapaKartorna. Kom ihåg att du får hjälp med att skriva metodkommentarer genom att skriva tre snedstreck precis ovanför metodens deklaration.

/// <summary>
/// Fyller spelarens och datorns kartor med tomma rutor
/// </summary>
static void SkapaKartorna()
{
    for (int y = 0; y < kartHöjd; y++)
    {
        for (int x = 0; x < kartBredd; x++)
        {
            spelarensKarta[x, y] = "O";
            datornsKarta[x, y] = "O";
        }
    }
}

Nästa steg blir att placera ut skepp, representerade av X, på spelarens och datorns kartor. För att spelet ska vara roligt att spela så bör spelaren själv få välja var skeppen ska placeras och datorns skepp borde placeras slumpmässigt. Det är dock jobbigt att testa programmet om man inte vet var datorns skepp har slumpats fram, och därför låter vi tills vidare denna metod placera ut två skepp på varje karta på några förutbestämda koordinater.

/// <summary>
/// Placerar X på några av spelarens och datorns platser
/// </summary>
static void PlaceraSkepp()
{
    spelarensKarta[1, 3] = "X";
    spelarensKarta[2, 0] = "X";
    datornsKarta[3, 2] = "X";
    datornsKarta[1, 3] = "X";
}

Med dessa två metoder färdigskrivna så kan vi anropa dem från programmets Main-metod.

SkapaKartorna();
PlaceraSkepp();
// Spela själva spelet

Metoden som sköter att själva spelets spelas kommer att vara längre än de andra metoderna och dessutom ska den anropa andra mindre metoder som vi kommer att skriva, därför behöver den planeras mer i detalj.

Planering av SpelaSänkaSkepp

En omgång av Sänka skepp består av en mängd turer. Under varje tur kommer först användaren att få skjuta ett skott och därefter datorn. När spelaren och datorn har skjutit så måste vi kolla om någon av dem har vunnit genom att ha träffat alla av sin motståndarens skepp, annars så kör spelet vidare på en ny tur. En planering av själva spelet följer här.

// harNågonVunnit = false
// while (harNågonVunnit == false)
    // Rensa spelfönstret
    // Rita spelplanen
    // Läs in koordinater för ett nytt skott
    // Markera att rutan med dessa koordinater är skjuten
    // Slumpa ett skott för datorn
    // Kolla om någon har vunnit

För att metoden SpelaSänkaSkepp inte ska bli för lång så kommer vi att dela upp den i några mindre metoder. Vi kommer att skriva en metod som ritar ut spelplanen, d.v.s. båda spelarnas kartor, en metod som kollar om spelaren har vunnit samt en metod som kollar om datorn har vunnit. Vi kommer också att använda en ReadInt-metod som vi själva skriver för att läsa in heltal från användaren utan risk att programmet kan krascha.

Vi kommer att behöva något sätt att hålla reda på vilka rutor som spelaren och datorn har skjutit på. Det går att göra detta på flera olika sätt, vi väljer att göra det med hjälp av att skapa två nya tvådimensionella arrayer, en för spelaren och en för datorn. Dessa arrayer ska ha samma storlek som de andra tvådimensionella arrayerna men de ska bestå av bool-variabler istället som kan ha värdet true eller false. Vi lägger till dessa arrayer som nya klassvariabler i programmet.

static bool[,] spelarensSkott = new bool[kartBredd, kartHöjd];
static bool[,] datornsSkott = new bool[kartBredd, kartHöjd];

Vi uppdaterar metoden SkapaKartorna och ser till att den fyller båda dessa tvådimensionella arrayer med värdet false vilket innebär att ingen ruta är skjuten på än så länge.

/// <summary>
/// Fyller spelarens och datorns kartor med tomma rutor
/// </summary>
static void SkapaKartorna()
{
    for (int y = 0; y < kartHöjd; y++)
    {
        for (int x = 0; x < kartBredd; x++)
        {
            spelarensKarta[x, y] = "O";
            datornsKarta[x, y] = "O";
            spelarensSkott[x, y] = false;
            datornsSkott[x, y] = false;
        }
    }
}

För att markera att spelaren har skjutit på en ruta så kommer vi att ändra värdet av den rutan från false till true i spelarensSkott och när datorn skjuter ändrar vi rutans värde från false till true i datornsSkott.

Metoderna som SpelaSänkaSkepp anropar

Nästa steg blir att skriva de metoder som ska anropas från SpelaSänkaSkepp. Du bör själv skapa metoden SpelaSänkaSkepp nu och anropa den i slutet av programmets Main-metod. Fyll på metoden SpelaSänkaSkepp med anrop till de metoder som du kommer att skapa här för att se att de fungerar efter att du har skrivit dem. Den färdiga koden för SpelaSänkaSkepp kommer vi att kunna se först när vi har skrivit de metoder som ska anropas från SpelaSänkaSkepp.

När spelet spelas så ska spelplanen ritas ut efter varje tur. Spelplanen består av varje spelares karta och vi kommer att rita ut spelarens karta överst och datorns karta nederst. Eftersom spelarens karta inte är hemlig för den som spelar spelet ritar vi ut hela den och vi färgar alla rutor som är skjutna av datorn röda, övriga rutor kommer att ritas i den grå standardtextfärgen. Eftersom man inte ska se motståndarens karta när man spelar Sänka skepp så kan vi inte rita ut hela datorns karta, vi ritar bara ut de rutor som spelaren har skjutit på. De rutor som spelaren inte har skjutit på kommer att ritas ut med ett bindestreck.

/// <summary>
/// Ritar ut spelarens och datorns kartor
/// </summary>
static void RitaSpelplanen()
{
    Console.WriteLine("Spelarens karta");
    for (int y = 0; y < kartHöjd; y++)
    {
        for (int x = 0; x < kartBredd; x++)
        {
            if (datornsSkott[x, y] == true)
            {
                Console.ForegroundColor = ConsoleColor.Red;
            }
            Console.Write(spelarensKarta[x,y]);

            // Återställ färgen till grå
            Console.ForegroundColor = ConsoleColor.Gray;
        }
        Console.WriteLine();
    }

    Console.WriteLine();

    // Rita datorns karta
    Console.WriteLine("Datorns karta");
    for (int y = 0; y < kartHöjd; y++)
    {
        for (int x = 0; x < kartBredd; x++)
        {
            // Endast rutor som spelaren har skjutit på syns
            if (spelarensSkott[x, y] == true)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.Write(datornsKarta[x, y]);
            }
            else
            {
                Console.Write("-");
            }


            // Återställ färgen till grå
            Console.ForegroundColor = ConsoleColor.Gray;
        }
        Console.WriteLine();
    }
}

När du har skrivit denna metod är det lämpligt att testa den. En ofärdig testversion av metoden SpelaSänkaSkepp skulle kunna se ut som nedan. Innan spelplanen ska ritas ut så rensas hela konsollfönstret på text med metoden Console.Clear så att endast den senaste spelplanen syns i fönstret av estetiska skäl.

/// <summary>
/// Spelets huvudloop
/// </summary>
static void SpelaSänkaSkepp()
{
    bool harNågonVunnit = false;
    while (harNågonVunnit == false)
    {
        Console.Clear();
        RitaSpelplanen();
        // Läs in koordinater för ett nytt skott
        // Markera att rutan med dessa koordinater är skjuten
        // Slumpa ett skott för datorn
        // Kolla om någon har vunnit

        Console.ReadKey();
    }
}

Det är viktigt att försöka testa sitt program ofta så att du upptäcker fel så fort som möjligt, annars kan det bli väldigt svårt att hitta dem. I denna testversion av SpelaSänkaSkepp så anropar vi Console.ReadKey i slutet av while-loopen för att kartan inte ska ritas om och om igen, detta anrop till Console.ReadKey kommer vi senare ta bort när vi läser in koordinater för nästa skott från användaren.

Testa ditt program ofta

Det är alltid en bra idé att testa sina program ofta. Om man lägger till mycket eller ändrar koden på många ställen i ett program mellan två testkörningar så blir det svårare att hitta vad som är fel ifall programmet inte fungerar som det ska.

Vi ska nu skriva en metod som undersöker om spelaren har vunnit. Vi vet att spelaren inte har vunnit än så länge om det finns någon ruta som är markerad med X på datorns karta som spelaren inte har skjutit på. Om vi hittar någon sådan ruta returnerar metoden false. Om vi har gått igenom hela datorns karta och inte hittat någon sådan ruta så har spelaren vunnit och metoden returnerar true.

/// <summary>
/// Kollat om spelaren har vunnit
/// </summary>
/// <returns></returns>
static bool HarSpelarenVunnit()
{
    for (int y = 0; y < kartHöjd; y++)
    {
        for (int x = 0; x < kartBredd; x++)
        {
            if (datornsKarta[x, y] == "X" && spelarensSkott[x, y] == false)
            {
                return false;
            }
        }
    }

    return true;
} 

Vi skriver även en liknande metod för att undersöka om datorn har vunnit. Dessa metoder är väldigt lika i sin kod och hade kunnat ersättas av en metod med två parametrar, en parameter för den tvådimensionella arrayen med strängar och en för den tvådimensionella arrayen med bool-variabler. Vi har valt att ha två separata metoder för att koden ska se lite mer lättläst ut även om man egentligen ska försöka undvika att upprepa kod så mycket som vi har gjort i dessa metoder.

/// <summary>
/// Kollar om datorn har vunnit
/// </summary>
/// <returns></returns>
static bool HarDatornVunnit()
{
    for (int y = 0; y < kartHöjd; y++)
    {
        for (int x = 0; x < kartBredd; x++)
        {
            if (spelarensKarta[x, y] == "X" && datornsSkott[x, y] == false)
            {
                return false;
            }
        }
    }

    return true;
} 

Nu så har vi bara en metod kvar att skriva innan vi kan implementera metoden SpelaSänkaSkepp och det är en metod som läser in heltal som inte kan krascha när den används. Här kan vi kopiera in den ReadInt-metod som vi skrivit tidigare i boken.

/// <summary>
/// Läser in ett heltal från användaren
/// </summary>
/// <returns>Användarens heltal</returns>
static int ReadInt()
{
    int heltal;
    while (int.TryParse(Console.ReadLine(), out heltal) == false)
    {
        Console.WriteLine("Du skrev inte in ett heltal. Försök igen.");
    }
    return heltal;
}

Det som återstår nu är att sätta ihop programmet i metoden SpelaSänkaSkepp.

Metoder med arrayer som argument

Ytterligare en anledning till att vi gjorde två metoder för HarSpelarenVunnit och HarDatornVunnit istället för endast en är att denna metod i så fall hade behövt ha en array som ett argument. Det finns några saker man måste tänka på när man använder en array som argument som tas upp i nästa kapitel.

Implementering av SpelaSänkaSkepp

Här anropar vi nu de metoder som vi har skrivit tidigare. En sak att tänka på är hur användaren ska skriva in koordinaterna för var hens nya skott ska skjutas. Om spelplanen är 6 rutor bred, som den är just nu enligt en variabeln kartBredd, så ska användaren få skriva in en X-koordinat från 1 till 6. Motsvarande index i vår tvådimensionella array kommer att vara 0 till 5 så vi måste minska den inskrivna X-koordinaten med 1 när vi ska använda den som index i arrayen.

/// <summary>
/// Spelets huvudloop
/// </summary>
static void SpelaSänkaSkepp()
{
    bool harNågonVunnit = false;
    while (harNågonVunnit == false)
    {
        Console.Clear();
        RitaSpelplanen();
        Console.WriteLine("Var vill du skjuta? (X)");
        int x = ReadInt();
        Console.WriteLine("Var vill du skjuta? (Y)");
        int y = ReadInt();
        spelarensSkott[x - 1, y - 1] = true;
        datornsSkott[slump.Next(kartBredd), slump.Next(kartHöjd)] = true;

        if (HarSpelarenVunnit())
        {
            Console.Clear();
            RitaSpelplanen();
            Console.WriteLine("Spelaren vann");
            Console.ReadKey();
            harNågonVunnit = true;
        }
        else if (HarDatornVunnit())
        {
            Console.Clear();
            RitaSpelplanen();
            Console.WriteLine("Datorn vann");
            Console.ReadKey();
            harNågonVunnit = true;
        }
    }
} 

Hela programmet

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

Hela programmet

När man skriver program av den här storleken som är uppdelade i många metoder så kan det ibland bli svårt att hitta den metod som man letar efter i koden. Du kan ta hjälp av Visual studio för att snabbare hitta runt med hjälp av ”Go to definition”. Testa att högerklicka på anropet till SpelaSänkaSkepp inuti Main-metoden och välj ”Go to definition” så kommer Visual Studio att hoppa ner till den plats i koden där du har skrivit denna metod.

Förslag på förändringar och utökningar av Sänka skepp

Här följer förslag på vad du själv kan göra för att bygga ut Sänka skepp. 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 ”Placering av skepp” och ”Menyprogram”.

Placering av skepp

Gör så att användaren själv får välja var hens skepp ska placeras när man ska spela Sänka skepp. Datorns skepp ska placeras ut slumpmässigt på kartan, se upp så att inte två skepp hamnar på samma ruta!

Menyprogram

De tidigare projektuppgifterna som vi har gjort har varit menyprogram. Omvandla Sänka skepp till ett menyprogram med följande alternativ Välj ett av följande alternativ. 1. Spela ”Sänka skepp” 2. Visa senaste vinnaren 3. Avsluta programmet Om ingen har vunnit spelet än ska den senaste vinnaren vara ”Ingen vinnare än”. Om datorn vann den senaste matchen ska ”Datorn” vara senaste vinnaren. Om spelaren vinner ska hen få skriva in sitt namn och detta ska visas när man väljer alternativ 2.

Oavgjort

Programmet ska berätta att matchen blev oavgjord om både spelaren och datorn vann samtidigt.

Programmets utseende och känsla

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

Ingen ogiltig inmatning

Spelaren ska inte kunna mata in ogiltiga X- eller Y-koordinater när hen ska skjuta. Förslagsvis så kan du ändra så att ReadInt-metoden har en valbar min-parameter och en valbar max-parameter som anger det minsta och det största värdet som den ska acceptera från användaren. Om det inmatade talet är för litet eller stort ska ReadInt-metoden berätta det och be användaren skriva in ett nytt tal.

Kartor sida vid sida

Rita ut spelarens och datorns kartor sida vid sida istället för ovanför och under varandra.

Annorlunda koordinatinmatning

Låt spelaren skriva in koordinaterna genom att skriva t.ex. ”A5” istället för X-koordinaten 1 och Y-koordinaten 5 eller ”C2” istället för X-koordinaten 3 och Y-koordinaten 2. Skriv ut namnet på varje rad och kolumn i kanterna av varje spelares karta.

Till din hjälp kan du skapa strängen string alfabetet = "ABCDEFGHIJKLMNOPQRSTUVXYZÅÄÖ". Vilka index har t.ex. bokstäverna A och C i denna sträng?

Inställningar

Skapa ett nytt menyalternativ som heter Inställningar. Här ska användaren få ställa in hur många skepp varje spelare placerar samt hur stora kartorna ska vara i X och Y-led.

Spara information i fil

Spara namnet på den senaste vinnaren samt de inställningar som användaren har gjort i menyalternativet Inställningar i en fil. Den senaste vinnarens namn och programmets inställningar ska läsas in när man startar programmet.

Fusk

Lägg till en hemlig fuskkod som man kan skriva i huvudmenyn, t.ex. ”FUSK”. När man skriver denna kod ska man slå på fuskläget i spelet som gör så att datorns spelplan visas vilket kan vara användbart när du vill testa ditt spel. Om man skriver fuskkoden och fuskläget redan är igång så ska det istället stängas av.

Annorlunda skeppsformer

I det riktiga spelet Sänka skepp består inte skeppen av enbart ett X utan av flera. Gör så att spelaren och datorn får skepp som består av flera X. Du skulle kunna låta spelaren/datorn få två skepp var som består av två X och två skepp som består av tre X. Ett av skeppen med två X ska placeras horisontellt och det andra vertikalt, detsamma ska gälla skeppen med tre X. Tänk på att skeppen inte får överlappa varandra!

Smartare AI (Artificiell intelligens)

Gör så att datorn inte skjuter helt slumpmässigt när den ska skjuta, den ska välja ruta lite smartare. Från början ska den slumpa helt var den skjuter, men när den träffar ett skepp så ska den skjuta på alla rutor som är runt denna ruta innan den går vidare och slumpar rutor igen. Om det finns en obeskjuten ruta jämte en ruta med ett träffat skepp ska datorn alltså skjuta där, annars väljer den en ruta slumpmässigt.

Förslag på egen projektuppgift – Se upp!

Skapa ett program som är ett spel där användaren ska akta sig för fallande bomber. Låt bomberna ritas ut som O och spelaren som ett X. Spelaren går hela tiden runt på marken (raden längst ner) och försöker att inte bli träffad av de fallande bomberna som rör sig en ruta neråt varje gång spelaren rör sig. Varje tur måste spelaren gå antingen till vänster eller höger, hen får inte stå still. Det ska skapas en ny bomb på en slumpad ruta på den översta raden varje gång spelaren rör sig. Målet för spelaren är att överleva så många turer av spelet som möjligt utan att bli träffad.

Här visas ett exempel på hur spelplanen skulle kunna se ut en bit in i ett spel där storleken på planen är 10 rutor bred och 6 rutor hög.

Tur 14
----------
      O    
  O       
  O       
     O    
O         
   O    X  
----------
Välj riktning (A = Vänster, D = Höger)

Spelaren valde på denna tur att gå till höger och fick då följande spelplan.

Tur 15
----------
        O 
      O    
  O       
  O       
     O    
O        X 
----------
Välj riktning (A = Vänster, D = Höger)

Bygg programmet som ett menyprogram och försök att själv komma på lämpliga utökningar till spelet.

Kommentarer