Bygg Python-spill med OOP: Multiplikasjonstabell-app

I denne artikkelen skal vi konstruere en applikasjon for multiplikasjonstabeller ved bruk av objektorientert programmering (OOP) i Python.

Du vil få praktisk erfaring med de grunnleggende prinsippene i OOP og hvordan de kan anvendes i en fullt operasjonell applikasjon.

Python er et programmeringsspråk som støtter flere paradigmer. Dette gir oss utviklere muligheten til å velge den mest passende tilnærmingen for hvert unike scenario og problem. Når vi snakker om objektorientert programmering, henviser vi til et av de mest populære paradigmene for å utvikle skalerbare applikasjoner i de senere årene.

Fundamentale aspekter ved OOP

La oss se nærmere på det sentrale konseptet i OOP i Python, nemlig klassene.

En klasse er en modell som definerer strukturen og funksjonaliteten til objekter. Denne modellen muliggjør opprettelsen av instanser, som er individuelle objekter som følger klassens definisjon.

En enkel bokklasse med attributter som tittel og farge, kan defineres som følger:

class Bok:
    def __init__(self, tittel, farge):
        self.tittel = tittel
        self.farge = farge

For å skape instanser av bokklassen, må vi kalle klassen og sende inn nødvendige argumenter:

# Opprettelse av instanser av Bok-klassen
blaa_bok = Bok("Den blå gutten", "Blå")
gronn_bok = Bok("Froskefortellingen", "Grønn")

En visuell representasjon av vårt nåværende program kan se slik ut:

Det bemerkelsesverdige er at når vi sjekker typen til `blaa_bok` og `gronn_bok` instanser, vil vi få resultatet «Bok».

# Skriver ut typen av bøkene

print(type(blaa_bok))
# <class '__main__.Bok'>
print(type(gronn_bok))
# <class '__main__.Bok'>

Med disse konseptene solid på plass, kan vi begynne å utvikle prosjektet vårt 😃.

Prosjektbeskrivelse

I utviklerhverdagen, viser det seg at vi ikke bruker mesteparten av tiden vår på å skrive kode. Ifølge thenewstack, bruker vi faktisk bare rundt en tredjedel av tiden vår på å skrive eller refaktorere kode.

De resterende to tredjedelene går med til å lese andres kode og analysere problemstillingen vi står ovenfor.

I dette prosjektet vil jeg derfor presentere en problemstilling, og vi vil analysere hvordan vi utvikler applikasjonen vår basert på den. Vi gjennomfører dermed hele prosessen, fra å tenke på løsningen til å implementere den med kode.

En lærer i barneskolen ønsker et spill for å teste multiplikasjonsferdighetene til elever i alderen 8 til 10 år.

Spillet skal inkludere et system for liv og poeng, der eleven starter med 3 liv og må oppnå et visst antall poeng for å vinne. Programmet skal vise en «taper»-melding hvis eleven mister alle sine liv.

Spillet skal ha to moduser: tilfeldig multiplikasjon og tabellbasert multiplikasjon.

I den første modusen vil eleven bli presentert for en tilfeldig multiplikasjon fra 1 til 10, og må svare korrekt for å tjene ett poeng. Hvis feil svar gis, mister eleven et liv og spillet fortsetter. Eleven vinner spillet når 5 poeng er oppnådd.

Den andre modusen skal vise en multiplikasjonstabell fra 1 til 10, hvor eleven skal legge inn resultatet av hver multiplikasjon. Hvis eleven mislykkes tre ganger, taper han/hun, men hvis to tabeller fullføres, er spillet vunnet.

Jeg er klar over at kravene kan virke litt omfattende, men jeg lover at vi vil løse dem i denne artikkelen 😁.

Del og hersk

Den viktigste ferdigheten innen programmering er evnen til å løse problemer. Det er essensielt å ha en strategi før man begynner å skrive kode.

Jeg anbefaler alltid å ta et større problem og dele det opp i mindre, mer håndterbare deler som kan løses både enkelt og effektivt.

Dersom du skal utvikle et spill, start med å dele det inn i de mest sentrale komponentene. Disse delproblemene vil være betydelig lettere å håndtere.

Deretter kan du få oversikt over hvordan du skal implementere og integrere alt med kode.

La oss lage et diagram som viser hvordan spillet vil se ut.

Denne grafiske fremstillingen viser relasjonene mellom objektene i applikasjonen vår. Som du ser, er de to sentrale objektene tilfeldig multiplikasjon og tabellmultiplikasjon. Det eneste de har felles er attributtene for poeng og liv.

Med all denne informasjonen i bakhodet, la oss dykke ned i koden.

Opprettelse av foreldreklassen for spillet

Når vi arbeider med objektorientert programmering, er vi opptatt av å finne den mest elegante måten å unngå repetisjon av kode. Dette er kjent som DRY (Don’t Repeat Yourself).

Merk: Dette målet handler ikke om å skrive færre linjer med kode (kodekvalitet må ikke måles ut fra dette), men om å abstrahere den mest brukte logikken.

I tråd med dette prinsippet, må den overordnede klassen i applikasjonen vår etablere strukturen og ønsket oppførsel for de to andre klassene.

La oss se hvordan vi skal gjøre dette.

class GrunnleggendeSpill:

    # Lengde for meldingens sentrering
    meldings_lengde = 60
    
    beskrivelse = ""    
        
    def __init__(self, poeng_for_seier, antall_liv=3):
        """Grunnleggende spillklasse

        Args:
            poeng_for_seier (int): Antall poeng som trengs for å fullføre spillet 
            antall_liv (int): Antall liv eleven har. Standard er 3.
        """
        self.poeng_for_seier = poeng_for_seier

        self.poeng = 0
        
        self.liv = antall_liv

    def hent_tall_input(self, melding=""):

        while True:
            # Henter brukerens input
            bruker_input = input(melding) 
            
            # Hvis input er numerisk, returner den
            # Hvis ikke, skriv ut en melding og gjenta
            if bruker_input.isnumeric():
                return int(bruker_input)
            else:
                print("Input må være et tall")
                continue     
             
    def skriv_velkomstmelding(self):
        print("PYTHON MULTIPLIKASJONS SPILL".center(self.meldings_lengde))

    def skriv_tapermelding(self):
        print("BEKLAGER, DU MISTET ALLE LIVENE DINE".center(self.meldings_lengde))

    def skriv_vinnermelding(self):
        print(f"GRATULERER, DU OPPNÅDDE {self.poeng}".center(self.meldings_lengde))
        
    def skriv_gjeldende_liv(self):
        print(f"Du har nå {self.liv} liv\n")

    def skriv_gjeldende_poeng(self):
        print(f"\nDin poengsum er {self.poeng}")

    def skriv_beskrivelse(self):
        print("\n\n" + self.beskrivelse.center(self.meldings_lengde) + "\n")

    # Grunleggende kjøringsmetode
    def kjør(self):
        self.skriv_velkomstmelding()
        
        self.skriv_beskrivelse()

Wow, dette ser ut som en ganske omfattende klasse. La meg gi en detaljert forklaring.

La oss først forstå klasseattributtene og konstruktøren.

Klasseattributter er i bunn og grunn variabler som er opprettet i klassen, men utenfor konstruktøren eller andre metoder.

Instansattributter derimot, er variabler som bare er definert i konstruktøren.

Hovedforskjellen mellom disse to ligger i deres tilgjengelighetsomfang. Klasseattributter er tilgjengelige både fra en instans og fra selve klassen. Instansattributter er derimot kun tilgjengelige fra en instans.

spill = GrunnleggendeSpill(5)

# Tilgang til klasseattributtet 'meldings_lengde' fra instansen
print(spill.meldings_lengde) # 60

# Tilgang til klasseattributtet 'meldings_lengde' fra klassen
print(GrunnleggendeSpill.meldings_lengde)  # 60

# Tilgang til instansattributtet 'poeng' fra instansen
print(spill.poeng) # 0

# Tilgang til instansattributtet 'poeng' fra klassen
print(GrunnleggendeSpill.poeng) # Attributtfeil

Et annet artikkel kan gå mer i dybden på dette emnet. Følg med for å lese den.

`hent_tall_input`-funksjonen brukes til å sikre at brukeren kun gir numerisk input. Denne metoden er designet for å spørre brukeren helt til et gyldig tall er gitt. Vi vil bruke den i barneklassene senere.

Utskriftsmetodene lar oss unngå å skrive ut de samme meldingene hver gang en hendelse inntreffer i spillet.

Til slutt er `kjør`-metoden bare en wrapper som klassene `TilfeldigMultiplikasjon` og `TabellMultiplikasjon` vil bruke for å samhandle med brukeren og gjøre alt funksjonelt.

Opprettelse av barneklassene

Etter å ha opprettet foreldreklassen, som definerer strukturen og noe av funksjonaliteten til appen vår, er det nå tid for å utvikle de faktiske spillmodusklassene ved å bruke arvens styrke.

Tilfeldig multiplikasjonsklasse

Denne klassen vil være ansvarlig for å kjøre den «første modusen» i spillet vårt. Den vil naturligvis bruke den tilfeldige modulen, som gir oss muligheten til å spørre brukeren om tilfeldige operasjoner fra 1 til 10. Her er en utmerket artikkel om tilfeldige (og andre viktige moduler) 😉.

import random # Modul for tilfeldige operasjoner
class TilfeldigMultiplikasjon(GrunnleggendeSpill):

    beskrivelse = "I dette spillet må du svare riktig på tilfeldige multiplikasjonsstykker\nDu vinner hvis du oppnår 5 poeng, eller taper hvis du mister alle livene dine"

    def __init__(self):
        # Antall poeng som trengs for å vinne er 5
        # Sender argumentet 5 til "poeng_for_seier"
        super().__init__(5)

    def hent_tilfeldige_tall(self):

        forste_tall = random.randint(1, 10)
        andre_tall = random.randint(1, 10)

        return forste_tall, andre_tall
        
    def kjør(self):
        
        # Kaller foreldreklassen for å skrive ut velkomstmeldingene
        super().kjør()
        

        while self.liv > 0 and self.poeng_for_seier > self.poeng:
            # Henter to tilfeldige tall
            tall1, tall2 = self.hent_tilfeldige_tall()

            operasjon = f"{tall1} x {tall2}: "

            # Ber brukeren svare på operasjonen 
            # Forhindrer feil verdier
            bruker_svar = self.hent_tall_input(melding=operasjon)

            if bruker_svar == tall1 * tall2:
                print("\nSvaret ditt er riktig\n")
                
                # Legger til ett poeng
                self.poeng += 1
            else:
                print("\nBeklager, svaret ditt er feil\n")

                # Trekker fra ett liv
                self.liv -= 1
            
            self.skriv_gjeldende_poeng()
            self.skriv_gjeldende_liv()
            
        # Utføres kun når spillet er over
        # Og ingen av betingelsene er sanne
        else:
            # Skriver ut den endelige meldingen
            
            if self.poeng >= self.poeng_for_seier:
                self.skriv_vinnermelding()
            else:
                self.skriv_tapermelding()

Her har vi nok en massiv klasse 😅. Men som jeg nevnte tidligere, er det ikke antall linjer som teller, det er hvor lesbart og effektivt det er. Og det beste med Python er at det tillater utviklere å skrive ren og forståelig kode, nesten som vanlig engelsk.

Denne klassen har en ting som kan forvirre deg, men jeg skal forklare det så enkelt som mulig.

    # Foreldreklasse
    def __init__(self, poeng_for_seier, antall_liv=3):
        "...
    # Barneklasse
    def __init__(self):
        # Antall poeng som trengs for å vinne er 5
        # Sender argumentet 5 til "poeng_for_seier"
        super().__init__(5)

Konstruktøren i barneklassen kaller `super`-funksjonen, som refererer til foreldreklassen (`GrunnleggendeSpill`). Det forteller Python:

Fyll ut attributtet `poeng_for_seier` i foreldreklassen med 5!

Det er ikke nødvendig å sette inn `self` i `super().__init__()` delen, da vi kaller `super` inne i konstruktøren, noe som ville være overflødig.

Vi bruker også `super`-funksjonen i `kjør`-metoden, og vi vil se hva som skjer i den kodebiten.

    # Grunleggende kjøringsmetode
    # Foreldremetode
    def kjør(self):
        self.skriv_velkomstmelding()
        
        self.skriv_beskrivelse()
    def kjør(self):
        
        # Kaller foreldreklassen for å skrive ut velkomstmeldingene
        super().kjør()
        
        .....

Som du kanskje merker, skriver `kjør`-metoden i foreldreklassen ut velkomst- og beskrivelsesmeldingen. Det er imidlertid en god idé å beholde denne funksjonaliteten, samtidig som vi legger til ekstra i barneklassene. Vi bruker `super` for å kjøre all kode fra foreldremetoden før vi fortsetter med neste del.

Den andre delen av `kjør`-funksjonen er ganske enkel. Den ber brukeren om et tall sammen med en melding om hvilken operasjon som skal besvares. Deretter sammenlignes resultatet med den faktiske multiplikasjonen, og om de er like, legges det til et poeng, ellers trekkes et liv.

Det er verdt å nevne at vi bruker `while-else`-løkker. Dette overskrider omfanget av denne artikkelen, men jeg skal publisere en om det i løpet av noen dager.

Til slutt, funksjonen `hent_tilfeldige_tall` bruker `random.randint` som returnerer et tilfeldig heltall innenfor et angitt område. Deretter returnerer den et tuppel med to tilfeldige heltall.

Tabell Multiplikasjonsklasse

Den «andre modusen» må vise spillet i et multiplikasjonstabellformat, og sikre at brukeren svarer riktig på minst 2 tabeller.

For dette formålet vil vi igjen bruke `super` og endre foreldrenes klasseattributt `poeng_for_seier` til 2.

class TabellMultiplikasjon(GrunnleggendeSpill):

    beskrivelse = "I dette spillet må du løse hele multiplikasjonstabellen korrekt\nDu vinner hvis du løser 2 tabeller"
    
    def __init__(self):
        # Trenger å fullføre 2 tabeller for å vinne
        super().__init__(2)

    def kjør(self):

        # Skriver ut velkomstmeldinger
        super().kjør()

        while self.liv > 0 and self.poeng_for_seier > self.poeng:
            # Henter et tilfeldig tall
            tall = random.randint(1, 10)            

            for i in range(1, 11):
                
                if self.liv <= 0:
                    # Sikrer at spillet ikke kan fortsette 
                    # hvis brukeren går tom for liv

                    self.poeng = 0
                    break 
                
                operasjon = f"{tall} x {i}: "

                bruker_svar = self.hent_tall_input(melding=operasjon)

                if bruker_svar == tall * i:
                    print("Flott! Svaret ditt er riktig")
                else:
                    print("Beklager, svaret ditt er ikke riktig") 

                    self.liv -= 1

            self.poeng += 1
            
        # Utføres kun når spillet er over
        # Og ingen av betingelsene er sanne
        else:
            # Skriver ut den endelige meldingen
            
            if self.poeng >= self.poeng_for_seier:
                self.skriv_vinnermelding()
            else:
                self.skriv_tapermelding()

Som du ser, endrer vi bare `kjør`-metoden for denne klassen. Dette er arvens magi: vi skriver logikken en gang og bruker den flere steder, og glemmer det 😅.

I `kjør`-metoden bruker vi en for-løkke for å hente tallene fra 1 til 10 og bygger operasjonen som vises for brukeren.

Nok en gang vil `while`-løkken avsluttes hvis alle livene er brukt opp eller poengene som trengs for å vinne er oppnådd, og vinner- eller tapermeldingen vil vises.

JA, vi har laget de to modusene i spillet, men ingenting vil skje hvis vi kjører programmet nå.

La oss fullføre programmet ved å implementere modusvalget, og instansiere klassene basert på det valget.

Implementering av valg

Brukeren skal kunne velge hvilken modus de vil spille. La oss se hvordan du implementerer det.

if __name__ == "__main__":

    print("Velg spillmodus")

    valg = input("[1],[2]: ")

    if valg == "1":
        spill = TilfeldigMultiplikasjon()
    elif valg == "2":
        spill = TabellMultiplikasjon()
    else:
        print("Vennligst velg en gyldig spillmodus")
        exit()

    spill.kjør()

Først ber vi brukeren om å velge mellom 1 eller 2 moduser. Hvis input ikke er gyldig, vil skriptet stoppe å kjøre. Hvis brukeren velger den første modusen, vil programmet kjøre tilfeldig multiplikasjons spillmodus, og hvis brukeren velger den andre, vil tabell multiplikasjonsmodusen kjøres.

Slik vil det se ut.

Konklusjon

Gratulerer, du har bygget en Python-app med objektorientert programmering.

All koden er tilgjengelig i Github-depotet.

I denne artikkelen har du lært å:

  • Bruke Python-klassekonstruktører
  • Utvikle en funksjonell applikasjon med OOP
  • Bruke `super`-funksjonen i Python-klasser
  • Bruke de grunnleggende konseptene i arv
  • Implementere klasse- og instansattributter

Lykke til med kodingen 👨‍💻

Utforsk deretter noen av de beste Python IDE-ene for økt produktivitet.