Kapittel 7 Første hovedprosjekt


Du har nå gått gjennom all teorien for første halvdel av boken. Det er endelig på tide å gjøre et litt større prosjekt. Du skal nå programmere det klassiske spillet tre på rad, som noen også kaller tripp-trapp-tresko. Her skal du få brukt lister inni andre lister, løkker, betingede setninger, input fra brukeren, funksjoner, og mer. Dette prosjektet vil bruke det viktigste av det du har lært i Python så langt. Det er helt avgjørende at du koder med i denne seksjonen for at du skal få noe ut av det. La oss komme i gang!

7.1 Introduksjon - Nedbrytning av spillet

Du har sikkert spilt tre på rad en gang på barneskolen. Spillet har to spillere, der den ene spilleren har tegnet X og den andre har tegnet O. Spillerne bytter på å ta turer og plasserer ned tegnet sitt i et rutenett med 9 ruter fordelt på 3 rader og 3 kolonner. Det er kun lov å sette ned tegnet sitt i en tom rute. Målet med spillet er å få tre på rad av sitt eget tegn. Dette kan enten være langs en linje eller en diagonal. Hvis ikke noen av spillerne får tre på rad før brettet er fullt, blir spillet uavgjort.

drawing

Finn et papirark og tegn opp et par rutenett som beskrevet ovenfor. Spill noen spill tre på rad med deg selv der du styrer begge spillerne. Det å gjøre seg godt kjent med det programmene våre representerer er en god vane. Selv med noe så enkelt som spillet tre på rad så gir et par gjennomførte spill oss en bedre forståelse. Dersom vi misforstår spillet fundamentalt vil ikke all verdens Python-kunnskaper hjelpe oss.

Vi kommer til å angripe prosjektet med å bryte spillet ned i tre komponenter:

  • Brettet

  • Handlingene til spillerne

  • Reglene som avgjør spillet

Åpenbart er de tre komponentene relatert til hverandre. Spillerne bruker brettet til å spille spillet. Det er også reglene for spillet som motiverer spillerne til å ta valgene de gjør. Likevel kan brettet i isolasjon bygges uten de to andre komponentene. Handlingene til spillerne kan også bli innhentet uten at vi har implementert alle reglene for spillet.

Dette gir oss en hensiktsmessig strategi for å strukturere prosjektet. Vi starter med å simulere brettet. Deretter tar vi inn handlingene til spillerne. Til slutt implementerer vi reglene som bestemmer hvem som vinner spillet.

7.2 Oppsett av brettet

Lag deg en ny Python-fil som heter tre_på_rad.py. La oss begynne med å sette opp brettet. En måte å gjøre dette på er å representere brettet som en ordinær liste:

brett = [
    '_', '_', '_', 
    '_', '_', '_', 
    '_', '_', '_'
]

Her har jeg brukt understrekingstegnet _ for å indikere at en brettposisjon er tom. Vi kunne like så godt brukt en tom streng eller et annet symbol så lenge det ikke er tegnene X eller O som spillerne bruker.

Problemet med vår representasjon av brettet er at lister med enkeltelementer ikke er todimensjonale, men bare endimensjonale. Jeg har, ved å legge inn nye linjer, skrevet listen i koden over slik at den ser todimensjonal ut. Dette endrer ikke at den fundamentalt bare er endimensjonal.

Det å velge datatyper som ikke stemmer så godt som mulig med virkeligheten er et feiltrinn. Vi kan lage en todimensjonal liste ved å legge lister inni andre lister som vi så i seksjon 4.6:

brett = [
    ['_', '_', '_'], 
    ['_', '_', '_'], 
    ['_', '_', '_']
]

I tillegg til å opprette brettet så kan det være greit å skrive det ut. Her kommer en enkelt print() funksjon til kort. Hvis du legger print(brett) til koden over så vil du se at det ikke ser særlig fint ut. Selv om jeg har lagt inn nye linjer i listen brett for at den skal se fin ut, så vil ikke dette bli representert når den blir skrevet ut. Vi må rett og slett lage egen funksjonalitet for å skrive ut brettet til terminalen:

# Setter opp brettet
brett = [
    ['_', '_', '_'], 
    ['_', '_', '_'], 
    ['_', '_', '_']
]

# Skriver ut brettet
print()
for rad in brett:
    for tegn in rad:
        print(f'| {tegn} ', end='')
    print('|')
print('\n')

Prøv å kjør koden over og sammenlign vår utskrift av brettet med linjen print(brett). Mye bedre, sant? De tomme print()-funksjonene legger til vertikalt mellomrom når vi skriver det ut, slik at det ikke blir så sammenklemt. Vi skriver to for-løkker for å komme oss inn til det innerste nivået. Tegnet blir skrevet ut sammen med litt horisontalt mellomrom og tegnet | for å få det til å se ut som et brett.

Standardoppførselen til print() er å legge til et linjeskift. Denne oppførselen er som oftest helt super. For å skrive ut brettet vårt er det likevel en plage. Vi ønsker ikke linjeskift etter hvert tegn i en rad. Vi kan modifisere dette med å bruke det nøkkelordbaserte argumentet end i funksjonen print(). Her er standardverdien \n, som representerer at vi hopper ned til en ny linje. Vi skriver end='', som indikerer at vi ikke ønsker at noe skal skje i slutten av print()-utsagnet. Når raden er helt ferdig så bruker vi print('|') for å legge på det siste symbolet | før vi hopper ned til en ny linje.

7.3 Handlingene til spillerne

Nå som brettet er på plass må vi hente inn handlingene til spillerne. Hver spiller kan forsøke å plassere sitt tegn på en av de ni rutene. Fremfor å ta inn et tall fra 1 til 9, så er det lettere å ta inn to tall som representerer de to dimensjonene:

# Tar inn handlingene
handling = input('Hvor vil du legge tegnet ditt? (f.eks. 3-2) ')
vertikalt, horisontalt = handling.split('-')
print(f'Vertikalt: {vertikalt}')
print(f'Horisontalt: {horisontalt}')

Her ber vi om at handlingen fra brukeren er på formen A-B, der A og B er tall mellom 1 og 3. Deretter bruker vi den innebygde strengmetoden .split() for å splitte strengen i to. De to nye strengene er de to tallene på hver sin side av bindestreken -. Dette ser vi når vi skriver ut variablene vertikalt og horisontalt.

Vi kan nå skrive kode som legger inn denne handlingen på brettet. La oss for nå anta at det er spilleren med tegnet X som gjør handlingen. Et første forsøk kan se slik ut:

# Setter opp brettet
brett = [
    ['_', '_', '_'], 
    ['_', '_', '_'], 
    ['_', '_', '_']
]

# Tar inn handlingene
handling = input('Hvor vil du legge tegnet ditt? (f.eks. 3-2) ')
vertikalt, horisontalt = handling.split('-')

# Plasserer tegnet på brettet
brett[int(vertikalt) - 1][int(horisontalt) - 1] = 'X'

# Skriver ut brettet
print()
for rad in brett:
    for tegn in rad:
        print(f'| {tegn} ', end='')
    print('|')
print('\n')

Koden som skriver ut brettet viser at endringen er slik vi forventer. Vi må bruke funksjonen int() siden både vertikalt og horisontalt er strenger. I tillegg må vi trekke fra en slik at vi bruker int(vertikalt) - 1 og int(horisontalt) - 1. Dette er fordi Python teller fra og med 0 som vi har diskutert tidligere.

Fremgangsmåten vi har gjort er et godt utgangspunkt. Likevel vil spillere bli forvirret om vi teller horisontalt eller vertikalt først når vi ber om informasjon. I tillegg er det ikke klart om nummereringen går fra topp til bunn eller motsatt. Hvor 3-1 er på brettet kan jo være i alle fire hjørnene basert på hvordan man teller. Spilleren vil forstå vår måte å nummerere på ved å spille spillet et par ganger, men dette er ikke veldig brukervennlig. Vi kommer til å fikse dette senere, men merker oss allerede nå at det er forbedringspotensial med spillet vårt.

For at begge spillerne skal spille turer etter hverandre kan vi bruke en uendelig while-løkke der vi bruker en sannhetsverdi spiller_X som holder styr på hvilken spiller sin tur det er:

# Setter opp brettet
brett = [
    ['_', '_', '_'], 
    ['_', '_', '_'], 
    ['_', '_', '_']
]

# Skriver ut brettet
print()
for rad in brett:
    for tegn in rad:
        print(f'| {tegn} ', end='')
    print('|')
print('\n')

spiller_X = True

while True:
    # Tar inn handlingene
    handling = input('Hvor vil du legge tegnet ditt? (f.eks. 3-2) ')
    vertikalt, horisontalt = handling.split('-')
  
    # Plasserer tegnet på brettet
    tegn = 'X'
    if not spiller_X:
        tegn = 'O'
    brett[int(vertikalt) - 1][int(horisontalt) - 1] = tegn
    
    # Skriver ut brettet
    print()
    for rad in brett:
        for tegn in rad:
            print(f'| {tegn} ', end='')
        print('|')
    print('\n')
    
    # Bytter spiller sin tur
    spiller_X = not spiller_X

Les nøye gjennom koden over. Senere skal vi sikre at while-løkken stopper når en spiller vinner eller det blir uavgjort. Vi bruker variabelen spiller_X til å bestemme hvilken spiller sin tur det er. Merk at etter vi har fullført handlingen til spilleren så bruker vi linjen spiller_X = not spiller_X. Dette er et triks for å bytte en sannhetsverdi til den andre mulige verdien. Så når spiller_X var True så vil not spiller_X være False og vice versa. Dette er en fiffig måte å bytte mellom spillerne.

7.4 Første omskriving i funksjoner

Nå har vi fått litt kode på plass. La oss allerede nå omskrive en del av koden til funksjoner slik at det blir lettere å jobbe videre med den. Hvis du ser på koden over så er det fire funksjoner vi kan lage:

  • En funksjon som lager brettet.

  • En funksjon som skriver ut brettet.

  • En funksjon som tar inn en handling fra en spiller.

  • En funksjon som plasserer tegnet på brettet.

Gjør et forsøk selv på å skrive fire funksjoner som har denne funksjonaliteten og deretter sett dem sammen i while-løkken. Her er min versjon av dette:

def opprett_brett(størrelse=3):
    """Oppretter et brett der antall rader og kolonner 
    er begge lik parameteren størrelse."""
    brett = []
    for rad in range(størrelse):
        tomme_ruter = []
        for kolonne in range(størrelse):
            tomme_ruter.append('_')
        brett.append(tomme_ruter)
    return brett


def skriv_ut_brett(brett):
    """Skriver ut brettet til terminalen."""
    print()
    for rad in brett:
        for tegn in rad:
            print(f'| {tegn} ', end='')
        print('|')
    print('\n')


def mottar_handling():
    """Tar imot en handling fra en spiller."""
    handling = input('Hvor vil du legge tegnet ditt? (f.eks. 3-2) ')
    return handling.split('-')


def plasser_tegn(brett, vertikalt, horisontalt, spiller_X):
    """Plasser tegnet til en spiller på brettet."""
    tegn = 'X'
    if not spiller_X:
        tegn = 'O'
    brett[int(vertikalt) - 1][int(horisontalt) - 1] = tegn


brett = opprett_brett()
skriv_ut_brett(brett)

spiller_X = True

while True:
    vertikalt, horisontalt = mottar_handling()
    plasser_tegn(brett, vertikalt, horisontalt, spiller_X)
    skriv_ut_brett(brett)
    spiller_X = not spiller_X

Ved å omskrive kode til funksjoner forstår vi bedre hvilke deler av programmet som avhenger av hvilken informasjon. Funksjonen mottar_handling() har ingen parametere fordi den trenger ikke å vite hvilken spiller sin tur det er, eller hvilken type brett vi spiller på. Funksjonen plasser_tegn() derimot trenger å vite brettet, den vertikale og horisontale posisjonen til handlingen, og hvilken spiller sin tur det er.

Innad i while-løkken trenger vi ikke lenger kommentarer siden funksjonsnavnene er klare. Utsagnet spiller_X = not spiller_X er også ganske standard kode for å bytte tur, så jeg tar vekk kommentaren for dette også. Her ser du at det ikke koster deg noe å legge på litt ekstra kommentarer i begynnelsen. Vi bare fjerner dem hvis vi ikke lenger føler de bidrar til bedre forståelse.

Merk også at jeg har gjort funksjonen opprett_brett() mer generell enn bare et enkelt 3x3 brett. Hvis du ikke spesifiserer noen argumenter når du kaller opprett_brett() vil standardverdien til parameteren størrelse være 3. Dette gir oss et 3x3 brett, og er det vi skal jobbe videre med. Når vi kommer til seksjon 7.6 skal vi se hvordan vi kan utvide spillet til å spille på et 4x4 brett med 16 ruter. Det er da heller ingenting i veien for å spille på et 10x10 brett med 100 ruter!

7.5 Reglene til spillet

Med koden vi har så langt vil spillet vare for alltid. Det er tre regler vi må skrive for at spillet skal fungere slik vi kjenner det:

  • Vi må sjekke om hele brettet er fullt.

  • Vi må sjekke om en rute allerede er blitt fylt ut før et tegn blir plassert det.

  • Vi må sjekke om noen av spillerne har fått tre på rad.

La oss ta for oss de tre reglene hver for seg.

7.5.1 Sjekk når brettet er fullt

Når brettet blir fylt opp kan en av to ting skje:

  • Den siste spilleren som la på tegnet sitt fikk tre på rad. Da skal den spilleren vinne.

  • Den siste spilleren som la på tegnet sitt fikk ikke tre på rad. Da skal det bli uavgjort.

Vi skal senere skrive logikk som sjekker om en spiller har vunnet. Dette vil dekke det første tilfellet over. Vi skal nå bare skrive logikk som sjekker om brettet er fullt. Dette vil gi resultatet uavgjort dersom den siste spilleren ikke fikk tre på rad.

Hvordan kan vi sjekke om brettet er fullt? Her er det flere måter som er mulige. Prøv å tenk ut noen måter dette kan sjekkes før du leser forslagene mine under:

  • Vi sjekker hvor mange tegn det er av både X og O på brettet. Hvis summen av dette er likt antall ruter på brettet er brettet fullt.

  • Vi sjekker hvor mange turer spillet har vart. Siden et nytt tegn bli lagt ut hver tur vet vi egentlig nøyaktig når brettet vil være fullt. Et standard 3x3 brett vil altså være ferdig etter 9 turer.

  • Vi sjekker om det er noen understreker _ igjen på brettet. Hvis det er ingen understreker igjen er brettet fullt.

Alle tre forslagene over vil fungere. Jeg velger å programmere den siste av dem siden den automatisk vil kunne håndtere større brett som vi skal se på i seksjon 7.6. Funksjonen som sjekker om brettet er fullt kan skrives slik:

def sjekk_brett_fullt(brett):
    """Sjekker om brettet er fullt, og returnerer da sannhetsverdien True."""
    for rad in brett:
        if '_' in rad:
            return False
    return True

Funksjonen sjekk_brett_fullt() går gjennom hver rad i brettet og sjekker om den inneholder en understrek _. Hvis en rad gjør dette kan ikke brettet være fullt. Da vil funksjonen sjekk_brett_fullt() returnere sannhetsverdien False. Hvis ingen av radene inneholder en understrek er brettet fullt. Da vil funksjonen sjekk_brett_fullt() returnere sannhetsverdien True.

Du kan nå kalle funksjonen sjekk_brett_fullt() innad i while-løkken til hovedprogrammet. Her bruker vi en if-setning for å sjekke om brettet er fylt opp. Hvis ja, bruker vi nøkkelordet break til å avslutte while-løkken og dermed også spillet:

while True:
    vertikalt, horisontalt = mottar_handling()
    plasser_tegn(brett, vertikalt, horisontalt, spiller_X)
    skriv_ut_brett(brett)
    if sjekk_brett_fullt(brett):
        print('Spillet er uavgjort!')
        break
    spiller_X = not spiller_X

Hvis du nå tester ut hele programmet har spillet i alle fall en avslutning. Siden spillet ikke kan bli vunnet enda vil det alltid bli uavgjort. Dette skal vi snart fikse. Først skal vi sikre at en spiller bare kan legge tegnet sitt i en tom rute.

7.5.2 Sjekk om en rute er tom

La oss nå skrive en funksjon sjekk_gyldig_trekk() som sjekker om en rute er tom før en spiller legger tegnet sitt der. Hvis ruten ikke er tom, så er ikke dette et gyldig trekk. Vi vil da be spilleren om å prøve på nytt. Hva trenger funksjonen sjekk_gyldig_trekk() av informasjon for å kunne avgjøre om trekket er gyldig? Med litt tenking kommer vi frem til følgende:

  • Funksjonen sjekk_gyldig_trekk() trenger informasjon om både brettet og posisjonen til det nye trekket.

  • Funksjonen sjekk_gyldig_trekk() trenger ikke informasjon om hvilken spiller det er som plasserer ut tegnet sitt. En spiller har ikke lov å legge tegnet sitt i en rute der det ligger et tegn fra før. Hvilket tegn som allerede ligger i ruten er ikke viktig.

Funksjonen sjekk_gyldig_trekk() kan implementeres slik:

def sjekk_gyldig_trekk(brett, vertikalt, horisontalt):
    """Sjekker om et trekk er gyldig, og returnerer da sannhetsverdien True."""
    if brett[int(vertikalt) - 1][int(horisontalt) - 1] == '_':
        return True
    return False

Funksjonen sjekk_gyldig_trekk() sjekker om ruten vi er interessert i er tom. Hvis ja, så er dette et gyldig trekk og funksjonen returnerer verdien True. Hvis nei, så er ruten allerede fylt opp av et annet tegn. Da returnerer funksjonen verdien False.

Du kan nå kalle funksjonen sjekk_gyldig_trekk() innad i while-løkken til hovedprogrammet. Her bruker vi en if-setning for å sjekke om trekket vi planlegger å gjøre ikke er gyldig:

while True:
    vertikalt, horisontalt = mottar_handling()
    if not sjekk_gyldig_trekk(brett, vertikalt, horisontalt):
        print('Dette er ikke en gyldig handling. Prøv igjen!')
        continue
    plasser_tegn(brett, vertikalt, horisontalt, spiller_X)
    skriv_ut_brett(brett)
    if sjekk_brett_fullt(brett):
        print('Spillet er uavgjort!')
        break
    spiller_X = not spiller_X

Dersom det er et ugyldig trekk bruker vi nøkkelordet continue. Hvorfor det? Da vil vi hoppe over hele iterasjonen videre. I den neste iterasjonen er det fortsatt den samme spilleren som har turen siden vi har hoppet over linjen spiller_X = not spiller_X. Test at programmet fungerer fint før vi fortsetter.

7.5.3 Sjekk når en spiller har vunnet

Nå kommer vi til den viktigste regelen. Vi må sjekke om en spiller har vunnet ved å få tre på rad. Vi lager oss en funksjon sjekk_vunnet_spill() som sjekker nettopp dette. Det er overraskende lite informasjon som funksjonen sjekk_vunnet_spill() trenger å ha tilgang til:

  • Funksjonen sjekk_vunnet_spill() trenger tilgang til brettet for å sjekke om noen har vunnet.

  • Funksjonen sjekk_vunnet_spill() trenger ikke tilgang til det nyeste trekket som har blitt gjort. Den kan bare evaluere hele brettet for å se om noen har fått tre på rad. Funksjonen sjekk_vunnet_spill() trenger heller egentlig ikke tilgang til hvilken spiller sin tur det er. Hvorfor ikke? Vi kommer til å kjøre funksjonen sjekk_vunnet_spill() hver eneste tur. Bare spilleren som akkurat har lagt på et tegn kan vinne. I tre på rad kan du ikke tape med å legge på et trekk.

Selv om funksjonen sjekk_vunnet_spill() ikke trenger tilgang til noe annet enn brettet er den likevel ganske komplisert. I denne seksjonen skal vi bare skrive sjekk_vunnet_spill() for et 3x3 brett med 9 ruter. I neste seksjon skal vi utvide sjekk_vunnet_spill() til å håndtere større brett.

Husk at det er flere måter å vinne i tre på rad. Du kan få tre på rad horisontalt, tre på rad vertikalt, eller tre på rad langs en diagonal. Her må vi altså gjøre tre forskjellige tester innad i samme funksjon for å teste alle tre tilfellene. La oss begynne med å sjekke diagonalene:

def sjekk_vunnet_spill(brett):
    """Sjekker om spillet har blitt vunnet, og 
    returnerer da sannhetsverdien True."""
    # Sjekk diagonalene
    if brett[0][0] == brett[1][1] == brett[2][2] != '_':
        return True
    if brett[2][0] == brett[1][1] == brett[0][2] != '_':
        return True
    return False

Les nøye over koden over og gjør deg komfortabel med at sjekk_vunnet_spill() så langt sjekker diagonalene på brettet. Vi kaller funksjonen sjekk_vunnet_spill() i while-løkken slik:

while True:
    vertikalt, horisontalt = mottar_handling()
    if not sjekk_gyldig_trekk(brett, vertikalt, horisontalt):
        print('Dette er ikke en gyldig handling. Prøv igjen!')
        continue
    plasser_tegn(brett, vertikalt, horisontalt, spiller_X)
    skriv_ut_brett(brett)
    if sjekk_vunnet_spill(brett):
        tegn = 'X'
        if not spiller_X:
            tegn = 'O'
        print(f'Gratulerer! Spilleren med tegnet {tegn} har vunnet!')
        break
    if sjekk_brett_fullt(brett):
        print('Spillet er uavgjort!')
        break
    spiller_X = not spiller_X

Merk at vi har plassert den nye if-setningen etter at vi har sjekket at trekket er gyldig, men før vi sjekker at brettet er fullt. Kodeblokken i den nye if-setningen er veldig lik koden vi skrev i funksjonen plasser_tegn() tidligere. Her er det litt gjenbruk av kode, men la oss vente litt med å omskrive dette.

Hvis du nå tester koden, kan du prøve å spille et spill der du lar en av spillerne vinne med å få tre på rad langs en av diagonalene. Det er viktig når du lager applikasjoner å teste dem når du legger til ny funksjonalitet. Da blir vi mer sikre på at alt er slik det skal være. Finner du feil må du leke detektiv for å finne ut hva som er galt.

La oss nå skrive ferdig funksjonen sjekk_vunnet_spill() for et 3x3 brett med 9 ruter. Det gjenstår å sjekke om spilleren har fått tre på rad langs en rad eller kolonne. Dette kan vi gjøre med en for-løkke slik:

def sjekk_vunnet_spill(brett):
    """Sjekker om spillet har blitt vunnet, og 
    returnerer da sannhetsverdien True."""
    # Sjekk radene
    for rad in range(3):
        if brett[rad][0] == brett[rad][1] == brett[rad][2] != '_':
            return True
    
    # Sjekk kolonnene
    for kolonne in range(3):
        if brett[0][kolonne] == brett[1][kolonne] == brett[2][kolonne] != '_':
            return True
    
    # Sjekk diagonalene
    if brett[0][0] == brett[1][1] == brett[2][2] != '_':
        return True
    if brett[2][0] == brett[1][1] == brett[0][2] != '_':
        return True
    return False

Verdien False vil bare bli returnert av funksjonen sjekk_vunnet_spill() dersom ingen spiller har tre på rad langs verken rader, kolonner, eller diagonaler. Spill igjen en runde med spillet der du lar en av spillerne vinne spillet ved å få tre på rad langs en rad eller en kolonne. Nå er grunnversjonen av spillet ferdig! Du kan nå spille tre på rad med et standard 3x3 brett med 9 ruter. I neste seksjon skal vi utvide programmet vårt slik at vi kan spille på et større brett.

7.6 Spille med et større brett

La oss nå utvide spillet tre på rad for å kunne spille på et større brett. Hva betyr dette i praksis for spillet?

Hvis vi spiller på et 4x4 brett med 16 ruter blir det veldig enkelt for spilleren som starter å få tre på rad. Derfor gjør vi det slik at målet er å få fire på rad når vi spiller på et 4x4 brett med 16 ruter. På samme måte må spillerne på et 7x7 brett med 49 ruter greie å få 7 på rad.

Det første vi må gjøre er å ta inn størrelsen på brettet fra spilleren. Vi kan modifisere koden rett før while-løkken slik:

størrelse = int(input('Hvor mange rader og kolonner skal brettet være? '))
brett = opprett_brett(størrelse=størrelse)

print('Brettet vi skal spille på ser slik ut: ')
skriv_ut_brett(brett)

Vi har tidligere sørget for at funksjonen opprett_brett() kan opprette vilkårlige størrelser på brettet. Variabelen størrelse holder altså både informasjonen om antall rader og kolonner i brettet, samt hvor mange tegn på rad hver spiller må ha for å vinne.

Det meste av logikken vi har skrevet til spillet fungerer helt fint med et større brett. Vi kan bruke samme kode som før for å sjekke om et trekk er gyldig eller om brettet er fullt. Begge funksjonene sjekk_gyldig_trekk() og sjekk_brett_fullt() fungerer tipp topp uten endringer.

Den store forskjellen er derimot logikken for å sjekke at en spiller har vunnet spillet. Hvis du leser over logikken i funksjonen sjekk_vunnet_spill() så vil du se at den er helt avhengig av at vi spiller på et 3x3 brett med 9 ruter. La oss nå omskrive sjekk_vunnet_spill() til å håndtere et større brett.

Størrelsen til brettet finner vi enkelt med koden len(brett). Siden brettet vårt er en liste med rader, så kan vi gå gjennom hver enkelt rad med en for-løkke. For hver rad kan vi sjekke om raden inneholder nok tegn til at en spiller vinner. Her passer listemetoden .count() perfekt:

def sjekk_vunnet_spill(brett):
    """Sjekker om spillet har blitt vunnet, og 
    returnerer da sannhetsverdien True."""
    størrelse = len(brett)
    
    # Sjekk radene
    for rad in brett:
        if rad.count('X') == størrelse or rad.count('O') == størrelse:
            return True
          
    # -- Kode for å sjekke kolonnene 
    
    # -- Kode for å sjekke diagonalene
    
    return False

Når vi skriver rad.count('X') == størrelse så sjekker vi om hele raden er fylt opp av symbolet X. Det samme gjør vi med symbolet O. I begge tilfellene returnerer vi sannhetsverdien True.

Ved første øyekast tenker du kanskje at kolonner vil være like enkelt som rader. Kolonner er litt mer involvert siden vi må først få hver kolonne som en liste vi kan sjekke. Vi bygger først opp hver kolonne som en liste og deretter bruker samme logikk som når vi sjekket radene:

def sjekk_vunnet_spill(brett):
    """Sjekker om spillet har blitt vunnet, og 
    returnerer da sannhetsverdien True."""
    størrelse = len(brett)
    
    # -- Kode for å sjekke radene
    
    # Sjekk kolonnene
    for kolonne_indeks in range(størrelse):
        kolonne = []
        for rad in range(størrelse):
            kolonne.append(brett[rad][kolonne_indeks])
        if kolonne.count('X') == størrelse or kolonne.count('O') == størrelse:
            return True
          
    # -- Kode for å sjekke diagonalene
    
    return False

Etter den indre for-løkken er utført vil listen kolonne ha verdiene i kolonnen med posisjon kolonne_indeks. Koden som følger for å sjekke om kolonnen er fylt opp av et tegn er lik som vi brukte tidligere.

Til slutt må vi sjekke diagonalene. Akkurat som med kolonnene så må vi først bygge lister som holder elementene til diagonalene. Dette krever litt prøving og feiling for de fleste siden man må holde tungen rett i munnen. Den fullstendige koden jeg får ser slik ut:

def sjekk_vunnet_spill(brett):
    """Sjekker om spillet har blitt vunnet, og 
    returnerer da sannhetsverdien True."""
    størrelse = len(brett)
    
    # -- Kode for å sjekke radene
    
    # -- Kode for å sjekke kolonnene

    # Sjekk diagonalene
    første_diagonal = []
    for posisjon in range(størrelse):
        første_diagonal.append(brett[posisjon][posisjon])
    if (første_diagonal.count('X') == størrelse 
        or første_diagonal.count('O') == størrelse):
        return True

    andre_diagonal = []
    for posisjon in range(størrelse):
        andre_diagonal.append(brett[størrelse - posisjon - 1][posisjon])
    if (andre_diagonal.count('X') == størrelse 
        or andre_diagonal.count('O') == størrelse):
        return True
    return False

Nå kan vi endelig spille på et større brett! Du kan prøve deg med fem på rad på et 5x5 brett med 25 ruter. Det er bare å invitere venner, partnere, naboer, og til og med fremmede på et sabla bra slag med tre på rad i terminalen din.

7.7 Hjelpefunksjoner og trefoldighetsoperatoren

La oss i denne og neste seksjon finpusse applikasjonen vår. Finpussingen vil ikke legge til ny funksjonalitet. Det er som regel lurt å bruke litt tid på å finpusse koden sin slik at den blir enklest mulig å både vedlikeholde og videreutvikle.

7.7.1 En hjelpefunksjon

Funksjonen sjekk_vunnet_spill() sjekker vi om en liste har nok X eller O tegn med metoden .count() fire separerte steder. Dette er mye gjentakende kode. La oss heller skrive en egen kort hjelpefunksjon som vi kan bruke til dette:

def inneholder_nok_tegn(samling, størrelse):
    """Sjekker om en liste har nok tegn av X eller O til å få en seier."""
    return samling.count('X') == størrelse or samling.count('O') == størrelse

Her er oppgaven til funksjonen inneholder_nok_tegn() såpass enkel at vi gjør hele logikken i en enkel linje. Funksjonen inneholder_nok_tegn() returnerer True dersom samling har nok av enten tegnet X eller O. Vi kan nå bruke hjelpefunksjonen inneholder_nok_tegn() innad i funksjonen sjekk_vunnet_spill() fire steder:

def sjekk_vunnet_spill(brett):
    """Sjekker om spillet har blitt vunnet, og 
    returnerer da sannhetsverdien True."""
    størrelse = len(brett)
    
    # Sjekk radene
    for rad in brett:
        if inneholder_nok_tegn(rad, størrelse):
            return True
    
    # Sjekk kolonnene
    for kolonne_indeks in range(størrelse):
        kolonne = []
        for rad in range(størrelse):
            kolonne.append(brett[rad][kolonne_indeks])
        if inneholder_nok_tegn(kolonne, størrelse):
            return True

    # Sjekk diagonalene
    første_diagonal = []
    for posisjon in range(størrelse):
        første_diagonal.append(brett[posisjon][posisjon])
    if inneholder_nok_tegn(første_diagonal, størrelse):
        return True

    andre_diagonal = []
    for posisjon in range(størrelse):
        andre_diagonal.append(brett[størrelse - posisjon - 1][posisjon])
    if inneholder_nok_tegn(andre_diagonal, størrelse):
        return True
    return False

Vi kaller funksjonen inneholder_nok_tegn() for en hjelpefunksjon66 til funksjonen sjekk_vunnet_spill() siden den hjelper med en del av beregningene. Funksjonen sjekk_vunnet_spill() er ikke en hjelpefunksjon siden den utfører en sentral del av funksjonaliteten til tre på rad. Som du sikkert forstår så er det en glidende overgang mellom hva vi anser som hjelpefunksjoner.

7.7.2 Trefoldighetsoperatoren

Både i funksjonen plasser_tegn() og i while-løkken som definerer spillet skriver vi:

tegn = 'X'
if not spiller_X:
    tegn = 'O'

Det er ingenting galt med denne koden. Likevel virker det å bruke tre linjer for noe så enkelt litt tungvint. I Python har vi en enklere måte å gjøre slike bytter på, nemlig slik:

tegn = 'X' if spiller_X else 'O'

Hvis du har gjort oppgavene i boken tidligere har du sett dette i oppgave 5 til seksjon 3.8. Dette kalles trefoldighetsoperatoren67. Dette er iblant nyttig for å forkorte enkle if-setninger.

Bør man alltid bruke trefoldighetsoperatoren? Ikke nødvendigvis! For det første kan det kun brukes på en enkel if-setning som tilegner en verdi til en variabel. For det andre kommer det i bunn og grunn an på leselighet. I koden over er if-setningen så enkel at den er fortsatt leselig ved å bruke trefoldighetsoperatoren. Tenk derimot at vi hadde hatt kode som dette:

ansatt = False
antall_treningsøkter = 33
pris_trening = 129.99
if antall_treningsøkter > 25 and ansatt:
    pris_trening = 99.99

Koden over kan vi prøve å skrive om ved å bruke trefoldighetsoperatoren:

ansatt = False
antall_treningsøkter = 33
pris_trening = 129.99 if antall_treningsøkter <= 25 or not ansatt else 99.99

Koden har blitt kortet ned hvis vi teller antall linjer. Likevel kommer dette på bekostning av at den siste linjen nå er ganske vrien å lese. Lesbarhet bør prioriteres høyt når du skriver kode. En tommelfingerregel er at når du skal velge mellom færre linjer og god leselighet så er det god leselighet som skal prioriteres.

7.8 Forbedret utskrift og en hovedfunksjon

Til slutt skal vi forbedre utskriften av brettet i spillet og samle løs kode inn i en hovedfunksjon som heter main(). Dette er de siste stegene som vil gjøre programmet vårt enkelt å både vedlikeholde og videreutvikle.

7.8.1 Bedre utskrift

Vi nevnte tidligere at det kan være uklart for en spiller hvordan vi nummererer brettet, og dermed uklart hvilken rute 3-1 representerer. Vi kan fikse dette med en litt mer omstendelig versjon av funksjonen skriv_ut_brett() slik:

def skriv_ut_brett(brett):
    """Skriver ut brettet til terminalen."""
    print()
    for indeks, rad in enumerate(brett):
        print(f'{indeks + 1}', end='')
        for tegn in rad:
            print(f'| {tegn} ', end='')
        print('|')
    print('   ', end='')
    for indeks in range(len(brett)):
        print(f'{indeks + 1}   ', end='')
    print('\n')

Her er det et par nye ting som skjer. Den innebygde funksjonen enumerate(), som du har møtt i oppgave 5 til seksjon 5.8, gir deg tilgang til både indeks og verdi. Så her refererer indeks til indeksen til elementet rad. Hvorfor er dette nyttig? For å lage tallene på venstre side av radene trenger vi indeksen til hver enkelt rad. Vi bruker uttrykket indeks + 1 siden Python teller fra null.

På slutten av skriv_ut_brett() har vi også skrevet en ny for-løkke som skriver ut tallene på bunnen av brettet. Kjører du koden kan du se det nye brettet. Det er nå klart hvordan nummereringen er gitt.

Det eneste som kanskje er uklart er hvorvidt vi ønsker å ta inn det horisontale tallet eller det vertikale tallet først. Dette kan beskrives til spilleren i starten av spillet:

størrelse = int(input('Hvor mange rader og kolonner skal brettet være? '))
brett = opprett_brett(størrelse=størrelse)

print('Brettet vi skal spille på ser slik ut: ')
skriv_ut_brett(brett)
print('Du spesifiserer først vertikal, deretter horisontal.')

Nå er det mye klarere hvordan spillerne skal gi inn informasjon for å plassere ut tegn.

7.8.2 En hovedfunksjon

Det siste vi skal gjøre er å innlemme koden som ligger utenfor funksjonene i en funksjon som heter main(). Dette er en standard praksis i Python. Navnet main() indikerer at dette er inngangen til hele programmet vårt. Det er main() sin oppgave å kjøre spillet og kalle på andre funksjoner for å hjelpe til. Slik ser main() ut:

def main():
    størrelse = int(input('Hvor mange rader og kolonner skal brettet være? '))
    brett = opprett_brett(størrelse=størrelse)

    print('Brettet vi skal spille på ser slik ut: ')
    skriv_ut_brett(brett)
    print('Du spesifiserer først vertikal, deretter horisontal.')
    
    spiller_X = True
    
    while True:
        vertikalt, horisontalt = mottar_handling()
        if not sjekk_gyldig_trekk(brett, vertikalt, horisontalt):
            print('Dette er ikke en gyldig handling. Prøv igjen!')
            continue
        plasser_tegn(brett, vertikalt, horisontalt, spiller_X)
        skriv_ut_brett(brett)
        if sjekk_vunnet_spill(brett):
            tegn = 'X' if spiller_X else 'O'
            print(f'Gratulerer! Spilleren med tegnet {tegn} har vunnet!')
            break
        if sjekk_brett_fullt(brett):
            print('Spillet er uavgjort!')
            break
        spiller_X = not spiller_X

Til slutt må vi huske å kalle funksjonen main() slik at alt starter når vi kjører spillet. Her hadde det holdt å skrive main() på siste linje i filen. Det vi derimot kommer til å skrive er dette:

if __name__ == '__main__':
    main()

Hva i all verden betyr dette? Denne if-setningen vil alltid være sann når du kjører programmet slik vi har gjort det. Så for vår del er dette det samme som å bare skrive main(). Forskjellen er når andre programmer ønsker å importere vår funksjonalitet i sine programmer. Da vil sjekken __name__ == '__main__' hindre at kode blir utført uten at det er hensikten. Du finner mer informasjon om __main__ her:

https://docs.python.org/3/library/__main__.html

Nå er koden ferdig finpusset og spillet fungerer supert. Hele koden for tre på rad spillet vårt oppsummerer jeg her, så kan du lese gjennom den en siste gang:

def opprett_brett(størrelse=3):
    """Oppretter et brett der antall rader og kolonner
    er begge lik parameteren størrelse."""
    brett = []
    for rad in range(størrelse):
        tomme_ruter = []
        for kolonne in range(størrelse):
            tomme_ruter.append('_')
        brett.append(tomme_ruter)
    return brett


def skriv_ut_brett(brett):
    """Skriver ut brettet til terminalen."""
    print()
    for indeks, rad in enumerate(brett):
        print(f'{indeks + 1}', end='')
        for tegn in rad:
            print(f'| {tegn} ', end='')
        print('|')
    print('   ', end='')
    for indeks in range(len(brett)):
        print(f'{indeks + 1}   ', end='')
    print()


def mottar_handling():
    """Tar imot en handling fra en spiller."""
    handling = input('Hvor vil du legge tegnet ditt? (f.eks. 3-2) ')
    return handling.split('-')


def plasser_tegn(brett, vertikalt, horisontalt, spiller_X):
    """Plasser tegnet til en spiller på brettet."""
    tegn = 'X' if spiller_X else 'O'
    brett[int(vertikalt) - 1][int(horisontalt) - 1] = tegn


def sjekk_brett_fullt(brett):
    """Sjekker om brettet er fullt, og returnerer da sannhetsverdien True."""
    for rad in brett:
        if '_' in rad:
            return False
    return True


def sjekk_gyldig_trekk(brett, vertikalt, horisontalt):
    """Sjekker om et trekk er gyldig, og returnerer da sannhetsverdien True."""
    if brett[int(vertikalt) - 1][int(horisontalt) - 1] == '_':
        return True
    return False


def inneholder_nok_tegn(samling, størrelse):
    """Sjekker om en liste har nok tegn av X eller O til å få en seier."""
    return samling.count('X') == størrelse or samling.count('O') == størrelse


def sjekk_vunnet_spill(brett):
    """Sjekker om spillet har blitt vunnet, og 
    returnerer da sannhetsverdien True."""
    størrelse = len(brett)
    
    # Sjekk radene
    for rad in brett:
        if inneholder_nok_tegn(rad, størrelse):
            return True
    
    # Sjekk kolonnene
    for kolonne_indeks in range(størrelse):
        kolonne = []
        for rad in range(størrelse):
            kolonne.append(brett[rad][kolonne_indeks])
        if inneholder_nok_tegn(kolonne, størrelse):
            return True

    # Sjekk diagonalene
    første_diagonal = []
    for posisjon in range(størrelse):
        første_diagonal.append(brett[posisjon][posisjon])
    if inneholder_nok_tegn(første_diagonal, størrelse):
        return True

    andre_diagonal = []
    for posisjon in range(størrelse):
        andre_diagonal.append(brett[størrelse - posisjon - 1][posisjon])
    if inneholder_nok_tegn(andre_diagonal, størrelse):
        return True
    return False


def main():
    størrelse = int(input('Hvor mange rader og kolonner skal brettet være? '))
    brett = opprett_brett(størrelse=størrelse)

    print('Brettet vi skal spille på ser slik ut: ')
    skriv_ut_brett(brett)
    print('Du spesifiserer først vertikal, deretter horisontal.')
    
    spiller_X = True
    
    while True:
        vertikalt, horisontalt = mottar_handling()
        if not sjekk_gyldig_trekk(brett, vertikalt, horisontalt):
            print('Dette er ikke en gyldig handling. Prøv igjen!')
            continue
        plasser_tegn(brett, vertikalt, horisontalt, spiller_X)
        skriv_ut_brett(brett)
        if sjekk_vunnet_spill(brett):
            tegn = 'X' if spiller_X else 'O'
            print(f'Gratulerer! Spilleren med tegnet {tegn} har vunnet!')
            break
        if sjekk_brett_fullt(brett):
            print('Spillet er uavgjort!')
            break
        spiller_X = not spiller_X


if __name__ == '__main__':
    main()