Extra¶
Vier op een rij: spelers¶
In week 5 schreef je de klasse Board, met host_game om twee mensen tegen
elkaar te laten spelen. Nu wil je ook tegen de computer kunnen spelen, of de
computer tegen zichzelf laten spelen.
Daar zit host_game in de weg. Die methode doet twee dingen tegelijk: ze houdt
het spel bij, met de beurten en de uitslag, en ze vraagt met input om een zet.
Een computerspeler typt niets in. Je zou in host_game kunnen controleren wie er
aan de beurt is, een mens of de computer, en voor elk een eigen stuk code
schrijven. Maar dan moet host_game veranderen bij elke nieuwe soort speler.
In deze opgave haal je die twee dingen uit elkaar. Het bord houdt het spel bij. Een
speler kiest een zet. host_game vraagt de speler die aan de beurt is om een
zet, en hoeft niet te weten of dat een mens is of een computer. Dat is
polymorfisme, net als special_move in het practicum.
Je werkt verder in je bestand vier_op_een_rij.py van week 5.
Het plan¶
Onderdeel |
Doet |
|---|---|
|
een nieuwe methode: in welke kolommen wint |
|
een speler: de steen die hij speelt, en een eenvoudige zet |
|
een subklasse van |
|
een subklasse van |
|
een speler die een vaste lijst zetten afwerkt, zonder subklasse van |
|
het spel, met twee spelers |
Elke speler heeft hetzelfde attribuut en dezelfde methode:
Van een speler |
Wat het is |
|---|---|
|
het attribuut met zijn steen: |
|
de methode die een kolom teruggeeft waarin een zet mag |
Meer gebruikt host_game niet van een speler. Een speler kijkt naar het bord via
de methoden en properties van Board, zoals allows_move en width, en komt
nooit aan _data.
Let op: host_game verandert. In week 5 was het host_game(self). In deze
opgave wordt het host_game(self, px, po), met twee spelers als argument. De
andere methoden van Board blijven zoals ze zijn.
Wat je gaat maken¶
Stap |
Wat |
Doet |
|---|---|---|
1 |
|
de kolommen waarin een speler met één zet wint |
2 |
|
een speler met een standaardzet |
3 |
|
een mens die zijn zet intypt |
4 |
|
het spel met twee spelers |
5 |
|
winnen als het kan, blokkeren als het moet |
6 |
|
een speler die een vaste lijst zetten afwerkt |
7 |
spelen |
de computer tegen zichzelf, en jij tegen de computer |
Zet de assertions onderaan in vier_op_een_rij.py, onder de klassen.
Stap 1: cols_to_win(self, ox)¶
Een nieuwe methode van Board. Ze geeft een lijst van de kolommen waarin ox met
één zet vier op een rij krijgt, van links naar rechts. Kan ox met één zet niet
winnen, dan is de lijst leeg.
| | | | | | | |
| | | | | | | |
| | | | | | | |
|O| | | | |X| |
|O| | |O| |X| |
|O| | |X|X|X| |
---------------
0 1 2 3 4 5 6
Op dit bord, set_board("334050505"), wint X in kolom 2 en 6 met vier op de
onderste rij, en in kolom 5 met vier boven elkaar. O wint in kolom 0.
Probeer elke kolom één voor één:
Kijk met
allows_moveof een zet in de kolom mag.Zet een steen
oxin de kolom, metadd_move.Kijk met
wins_forofoxnu gewonnen heeft. Zo ja, zet de kolom in de lijst.Haal de steen weer weg met
del_move, voordat je de volgende kolom probeert.
Na afloop is het bord dus precies zoals ervoor.
b = Board(7, 6)
b.set_board("334050505")
before = repr(b)
assert b.cols_to_win("X") == [2, 5, 6]
assert b.cols_to_win("O") == [0]
assert repr(b) == before
assert Board(7, 6).cols_to_win("X") == []
Stap 2: de klasse Player¶
Schrijf een klasse Player, buiten de klasse Board:
Methode |
Doet |
|---|---|
|
slaat de steen op in het attribuut |
|
geeft de steen van de tegenstander: |
|
geeft de meest linkse kolom van |
next_move loopt dus over de kolommen van het bord, van 0 tot board.width,
en geeft de eerste kolom terug waarvoor board.allows_move(col) waar is. Op een
vol bord wordt next_move niet aangeroepen, want dan is het spel al afgelopen.
b = Board(7, 6)
p = Player("X")
assert p.ox == "X"
assert p.opponent() == "O"
assert Player("O").opponent() == "X"
assert p.next_move(b) == 0
b.set_board("000000")
assert p.next_move(b) == 1
De meest linkse kolom is geen goede zet. Het is een standaard: een subklasse die niets beters weet, valt erop terug. Of een zet die niet goed is wel een goede standaard is, kun je je afvragen. In week 7 leer je hoe een superklasse kan afdwingen dat een subklasse een methode overschrijft.
Stap 3: HumanPlayer¶
Een mens is een speler. Schrijf HumanPlayer als subklasse van Player. Hij erft
de constructor en opponent, en overschrijft alleen next_move: die vraagt om
een kolom, net zo lang tot er een zet komt die mag. Dat is de kleine lus uit
host_game van week 5, die nu in de speler staat:
col = -1
while not board.allows_move(col):
col = int(input(f"Keuze van {self.ox}: "))
return col
Voer next_move een keer uit in de terminal, en typ eerst een kolom die niet
bestaat:
In [1]: b = Board(7, 6)
In [2]: HumanPlayer("X").next_move(b)
Keuze van X: 9
Keuze van X: 3
Out[2]: 3
next_move zet zelf geen steen. Ze kiest alleen een kolom: het zetten doet
host_game.
Stap 4: host_game(self, px, po)¶
Schrijf host_game opnieuw, zodat ze twee spelers meekrijgt. px speelt met
"X" en begint, po speelt met "O". Elke beurt gaat zo:
Vraag de speler die aan de beurt is om een kolom:
col = player.next_move(self).Zet zijn steen met
self.add_move(col, player.ox).Druk het bord af.
Kijk of deze speler gewonnen heeft, en daarna of het bord vol is. In beide gevallen meld je de uitslag en stop je met
break.Anders is de andere speler aan de beurt.
Druk net als in week 5 aan het begin een welkom af en het lege bord. De uitslag
is "X wint -- Gefeliciteerd!", "O wint -- Gefeliciteerd!" of
"Gelijkspel!".
Hint
Houd de speler die aan de beurt is bij in een variabele player, die begint als
px. Of het de beurt van px was, vraag je met player is px: is het hetzelfde
object?
In host_game staat nu geen input meer, en ook geen "X" of "O" om te kijken
wie er aan de beurt is. host_game weet alleen dat een speler een ox heeft en
een next_move. Welke next_move er draait, hangt af van de speler.
Speel het spel zoals in week 5, met twee mensen:
In [1]: b = Board(7, 6)
In [2]: b.host_game(HumanPlayer("X"), HumanPlayer("O"))
Speel de zetten van het voorbeeld van
week 5: X kiest 3, 2, 1 en
0, en O kiest 4, 4 en 2. Je ziet dezelfde vragen en dezelfde borden, en
aan het eind X wint -- Gefeliciteerd!.
Stap 5: SimpleAIPlayer¶
Een computerspeler is ook een speler. Schrijf SimpleAIPlayer als subklasse van
Player. Hij overschrijft next_move, en kiest zo:
Kan hij zelf met één zet winnen, dan speelt hij de eerste kolom uit
board.cols_to_win(self.ox).Anders: kan de tegenstander met één zet winnen, dan blokkeert hij de eerste kolom uit
board.cols_to_win(self.opponent()).Anders valt hij terug op de standaardzet van zijn superklasse:
super().next_move(board).
Hij kijkt dus maar één zet vooruit.
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | |X| | |
| | |O| |X| | |
| | |O| |X| | |
---------------
0 1 2 3 4 5 6
Op dit bord, set_board("42424"), wint X in kolom 4. Voor X is dat dus de
winnende zet, en voor O de zet die blokkeert.
b = Board(7, 6)
b.set_board("42424")
assert SimpleAIPlayer("X").next_move(b) == 4
assert SimpleAIPlayer("O").next_move(b) == 4
b = Board(7, 6)
b.set_board("334050505")
assert SimpleAIPlayer("X").next_move(b) == 2
assert SimpleAIPlayer("O").next_move(b) == 0
b = Board(7, 6)
assert SimpleAIPlayer("X").next_move(b) == 0
Op het tweede bord kan O zelf winnen in kolom 0, en dat gaat vóór het
blokkeren van X.
Stap 6: ScriptedPlayer¶
Een ScriptedPlayer speelt een lijst kolommen af die hij vooraf meekrijgt, één
per beurt. Hij is geen subklasse van Player, maar hij heeft wel een attribuut
ox en een methode next_move(board). Meer heeft host_game niet nodig: dat is
duck typing.
Methode |
Doet |
|---|---|
|
slaat |
|
geeft de volgende kolom uit de lijst: bij de eerste aanroep de eerste, bij de tweede de tweede, enzovoort |
Hint
Houd in een attribuut bij hoeveel zetten de speler al heeft gedaan. Dat getal is ook de index van de volgende kolom in de lijst.
Met twee ScriptedPlayers speel je een heel spel zonder iets in te typen. Zo kun
je host_game testen met een assertion. Dit is het spel uit week 5:
b = Board(7, 6)
b.host_game(ScriptedPlayer("X", [3, 2, 1, 0]), ScriptedPlayer("O", [4, 4, 2]))
assert b.wins_for("X")
assert not b.wins_for("O")
ScriptedPlayer controleert niet of een zet mag. De lijst moet dus kloppen.
Stap 7: spelen¶
Laat de computer tegen zichzelf spelen:
b = Board(7, 6)
b.host_game(SimpleAIPlayer("X"), SimpleAIPlayer("O"))
assert b.wins_for("O")
Er zit geen toeval in deze spelers, dus dit spel loopt elke keer hetzelfde. Kijk
naar de eerste zetten. Beide spelers vallen terug op de meest linkse kolom, tot X
drie stenen op de onderste rij heeft, in kolom 0, 1 en 2. Dan blokkeert O in
kolom 3. Aan het eind wint O.
Speel daarna zelf tegen de computer:
In [1]: b = Board(7, 6)
In [2]: b.host_game(HumanPlayer("X"), SimpleAIPlayer("O"))
Probeer dit: speel als X kolom 2, 3 en 4. Welke zet doet O daarna? En
waarom kun je dan toch winnen? Een speler die maar één zet vooruitkijkt, ziet niet
dat twee dreigingen tegelijk niet allebei te blokkeren zijn.
Tot slot¶
host_game speelt nu met elke speler die een ox heeft en een next_move: een
mens, een computer of een lijst zetten. Het bord houdt het spel bij, en een
speler kiest een zet. Een nieuwe soort speler is een nieuwe klasse, en host_game
verandert er niet voor. In week 7 schrijf je een speler die verder vooruitkijkt
dan één zet: dat is één subklasse meer.