Gra w monety ze sztuczną inteligencją¶
Wstęp¶
Gra w monety to jedna z gier dla dwóch graczy, której zasady można bardzo szybko i łatwo przyswoić. Na stole leżą sobie monety, a gracze grają na przemian. W swojej turze gracz może zabrać ze stołu \(1\), \(3\) lub \(4\) monety (o ile na stole pozostała odpowiednia liczba monet). Gracz, który zabierze ze stołu ostatnie monety, przegrywa.
Jak widać jest to gra z prostymi zasadami, chociaż nie koniecznie łatwo w nią wygrać. Dlatego jest to idealna gra do zaprezentowania działania sztucznej inteligencji.
Tak, dzisiaj stworzymy grę, w której naszym przeciwnikiem będzie komputer sterowany przez sztuczną inteligencję!
Czego się nauczysz¶
- Tworzenia klas.
- Wykorzystania sztucznej inteligencji do wykonywania ruchów.
Biblioteka¶
W celu stworzenia gracza sterowanego przez sztuczną inteligencję skorzystamy z biblioteki easyAI. Najpierw musimy ją jednak zainstalować. W terminalu wpisujemy poniższe polecenie i zatwierdzamy przyciskiem enter.
Klasa gry¶
Na początku nasza gra będzie miała charakter tekstowy.
Dopiero później, gdy podstawy będą już działać, dodamy do niej grafikę.
Zaczynamy więc od stworzenia pliku gameOfCoins.py
w którym zapiszemy implementację naszej tekstowej gry.
Importujemy biblioteki¶
Na początek wystarczy nam jedna biblioteka: easyAI. Ponieważ będziemy korzystać z wielu modułów tej biblioteki, zaimportujemy je wszystkie.
Tworzymy klasę gry¶
Nasza gra będzie opisana w osobnej klasie, ponieważ taki jest wymóg biblioteki. Klasa w programowaniu to coś jak schemat, według którego tworzymy różne obiekty. Można sobie wyobrazić, że klasa to instrukcja konstrukcji samochodu, a obiekt to konkretny samochód. Możemy mieć wiele samochodów stworzonych według tego samego schematu, ale różniących się np. kolorem lakieru. Podobnie możemy mieć kilka obiektów utworzonych na podstawie tej samej klasy.
Naszą klasę nazwiemy GameOfCoins i stworzymy w oparciu o klasę TwoPlayerGame z biblioteki easyAI.
Inicjalizujemy obiekt klasy¶
Klasa powinna mieć metodę, która pozwoli nam na utworzenie nowego obiektu (instancji) klasy.
Taka metoda ma specjalną nazwę: __init__
(skrót od initialization czyli inicjalizacja z angielskiego).
Przy towrzeniu nowej gry powinniśmy do niej przekazać jeden parametr: informację na temat graczy.
Wewnątrz funkcji inicjalizującej zapamiętujemy przekazany parametr w tworzonym obiekcie klasy, do którego odnosimy się poprzez słowo kluczowe self.
Powinniśmy także zdefiniować początkową liczbę monet na stole, np. \(17\). Liczbę monet zapamiętamy w zmiennej pile.
class GameOfCoins(TwoPlayerGame):
def __init__(self, players=None):
self.players = players
self.pile = 17
Na końcu potrzebujemy jeszcze iformacji o tym, który z graczy zaczyna jako pierwszy. Jak to zwykle bywa, zaczyna gracz o numerze jeden.
class GameOfCoins(TwoPlayerGame):
def __init__(self, players=None):
self.players = players
self.pile = 17
self.current_player = 1
Określamy dostępne ruchy¶
Przygotowaliśmy podwaliny pod naszą grę, teraz czas określić jej reguły. Zaczniemy od listy dozwolonych ruchów. W tym celu dopisujemy do naszej klasy metodę possible_moves.
Ruchy powinniśmy zwrócić w formie listy ruchów, a same ruchy powinny mieć format tekstowy (string). W naszej grze mamy trzy możliwe ruchy, tak jak wspomnieliśmy wcześniej.
Wykonujemy ruch¶
Wiemy już, jakie ruchy możemy wykonywać w grze. Co powinno się jednak wydarzyć po wykonaniu ruchu? Jak powinien się zmienić stan gry? To musimy zdefiniować za pomocą metody make_move, która jako parametr przyjmuje ruch do wykonania.
Po wykonaniu wybranego ruchu nasza liczba dostępnych monet powinna się zmniejszyć o liczbę zgodną z ruchem. Odejmujemy więc wartość ruchu od zmiennej pile, pamiętając o tym, że najpierw musimy zamienić ruch z tekstu na liczbę całkowitą.
Koniec gry¶
Czas zadecydować, kiedy gra się kończy. To określamy za pomocą funkcji is_over.
Nasza gra kończy się, gdy skończą się monety na stole. Sprawdzamy więc stan pozostałych monet i zwracamy stosowną informację.
Wyświetlanie stanu gry¶
W celu ułatwienia sobie rozgrywki, tak żebyśmy nie musieli wszystkiego pamiętać, warto co każdy ruch wyświetlać stan obecnej gry. Do tego posłuży nam metoda show.
Z perspektywy gracza najważniejsza jest liczba pozostałych na stole monet, taką więc informację wyświetlamy na ekranie.
class GameOfCoins(TwoPlayerGame):
...
def show(self):
print(f"{self.pile} monet pozostało na stole")
Punktacja AI¶
Aby sztuczna inteligencja mogła nauczyć się grać w naszą grę, musi wiedzieć, kiedy wygrywa, a kiedy nie. W tym celu dołożymy do naszej gry punktację, którą zdefiniujemy z pomocą metody scoring.
Punktacja będzie zależna od tego, ile monet pozostało w grze. Gdy monety się skończyły, to znaczy, że sztuczna inteligencja wygrała i powinna dostać punkty. W każdym innym przypadku nie przyznajemy punktów.
class GameOfCoins(TwoPlayerGame):
...
def scoring(self):
if self.pile <= 0:
return 100
else:
return 0
Pełna klasa gry¶
Tak powinna wyglądać teraz nasza klasa gry w monety.
class GameOfCoins(TwoPlayerGame):
def __init__(self, players=None):
self.players = players
self.pile = 17
self.current_player = 1
def possible_moves(self):
return ["1", "3", "4"]
def make_move(self, move):
self.pile -= int(move)
def is_over(self):
return self.pile <= 0
def show(self):
print(f"{self.pile} monet pozostało na stole")
def scoring(self):
if self.pile <= 0:
return 100
else:
return 0
Tworzymy grę i gramy!¶
Pod naszą klasą, w tzw. kodzie głównym, utworzymy następującą instrukcję warunkową:
Dzięki zastosowaniu takiej konstrukcji, kod który zaraz napiszemy wykona się tylko, gdy uruchomimy ten konkretny plik gry. Będzie to przydatne później, gdy będziemy korzystać z właśnie tworzonego pliku ptzy tworzeniu interfejsu graficznego.
W celu utworzenia gry potrzebujemy informacji na temat graczy. Pierwszy z nich będzie człowiekiem, a drugi będzie sztuczną inteligencją. Sztuczna inteligencja musi działać według jakiegoś algorytmu, który na początku musimy zdefiniować.
Skorzystamy z algorytmu Negamax. Jako parametr podajemy liczbę kroków naprzód, które sztuczna inteligencja ma "przewidywać".
Teraz tworzymy obiekt naszej gry. Jako parametr podajemy listę złożoną z dwóch graczy: gracza sterowanego przez człowieka (Human_Player) i gracza sterowanego przez sztuczną inteligencję (AI_Player).
if __name__ == "__main__":
algorithm = Negamax(13)
game = GameOfCoins([Human_Player(), AI_Player(algorithm)])
Na koniec pozostało nam uruchomić naszą grę i zagrać! Czy uda Ci się pokonać sztuczną inteligencję? Spróbuj różnych parametrów przy definiowaniu algorytmu Negamax. Sprawdź jak wpływa to na przebieg rozgrywki.
if __name__ == "__main__":
algorithm = Negamax(13)
game = GameOfCoins([Human_Player(), AI_Player(algorithm)])
game.play()
Pełna gra¶
from easyAI import *
class GameOfCoins(TwoPlayerGame):
def __init__(self, players=None):
self.players = players
self.pile = 17
self.current_player = 1
def possible_moves(self):
return ["1", "3", "4"]
def make_move(self, move):
self.pile -= int(move)
def is_over(self):
return self.pile <= 0
def show(self):
print(f"{self.pile} monet pozostało na stole")
def scoring(self):
if self.pile <= 0:
return 100
else:
return 0
if __name__ == "__main__":
algorithm = Negamax(13)
game = GameOfCoins([Human_Player(), AI_Player(algorithm)])
game.play()
Gra z grafiką¶
Teraz zajmiemy się tworzeniem graficznej wersji naszej gry z wykorzystaniem Pygame Zero.
Grafiki do pobrania¶
Zanim zaczniemy, pobierz poniższe grafiki, rozpakuj i umieść w katalogu images w projekcie gry.
Szablon gry¶
Na początku utwórz nowy plik gameOfCoinsPygame.py. Wewnątrz umieszczamy standardowy szablon.
import pgzrun
import random
WIDTH = 840
HEIGHT = 600
def draw():
screen.fill("white")
def update():
pass
pgzrun.go()
Biblioteki¶
Poza standardowymi bibliotekami pgzrun i random będą nam jeszcze potrzebne dwie inne: easyAI oraz utworzona przez nas "biblioteka" gameOfCoins. Na samej górze dopisujemy więc:
Inicjalizacja¶
Ponieważ kilka elementów musimy sobie przygotować przed uruchomieniem gry (np. monety, obiekt gry), to utworzymy sobie nową funkcję, na końcu, zaraz przed pgzrun.go()
.
Naszą funkcję wywołamy sobie zaraz przed uruchomieniem gry.
Tworzymy obiekt gry¶
W naszej graficznej wersji będziemy korzystać z wcześniej przygotowanej klasy GameOfCoins.
W tym celu na początku kodu, zaraz przed draw()
, tworzymy sobie zmienną game, która na wstępnie będzie miała przypisaną pustą wartość.
W części inicjalizującej utworzymy obiekt naszej gry, a także zdefiniujemy graczy, podobnie jak robiliśmy wcześniej.
...
def init():
global game
algorithm = Negamax(13)
game = GameOfCoins([Human_Player(), AI_Player(algorithm)])
...
Przygotowujemy monety¶
Monety będziemy przechowywać w liście, którą tworzymy na początku kodu:
Nasze monety przygotujemy sobie w części inicjalizującej.
...
def init():
...
x = 55
y = 50
for i in range(1, game.pile + 1):
coins.append(Actor("coin", (x, y)))
x += 84 + 20
if i % 8 == 0:
y += 84 + 30
x = 55
...
Wyświetlamy monety na ekranie w części rysującej draw.
Po uruchomieniu powinniśmy zobaczyć kilka rzędów monet na ekranie.
Przygotowujemy dostępne ruchy¶
Dostępne dla gracza ruchy będziemy reprezentować za pomocą kości do gry. Na początku przygotowujemy pustą listę kości.
Teraz czas wypełnić naszą listę odpowiednimi wartościami.
...
def init():
...
x = WIDTH / 2 - (len(game.possible_moves()) * 88 - 20) / 2 + 34
y = HEIGHT - 88
for move in game.possible_moves():
dices.append(Actor(f"dice{move}", (x, y)))
x += 68 + 20
...
Możemy już wyświetlić kości na ekranie.
Jak teraz uruchomimy grę, powinniśmy zobaczyć kości z możliwymi do wykonania ruchami.
Usuwamy monety¶
Zanim przejdziemy do wykonywania ruchów to przyda nam się dodatkowa funkcja do usuwania monet z planszy, którą nazwiemy remove_coins. Umieszczamy ją przed funkcją init.
Odczytujemy ruch gracza¶
Teraz możemy przejść do odczytania ruchu gracza. Tworzymy funkcję on_mouse_down, która pozwala nam odczytywać kliknięcia myszy. Umieszczamy ją pod funkcją update.
...
def update():
...
def on_mouse_down(pos):
for i in range(len(dices)):
if dices[i].collidepoint(pos) and game.current_player == 1:
move = game.possible_moves()[i]
if int(move) <= game.pile:
game.play_move(move)
remove_coins(int(move))
...
Wykonujemy ruch AI¶
Teraz przyszła wreszcie pora na ruch sztucznej inteligencji. Ruchy AI będziemy wykonywać w części aktualizującej update.
...
def update():
if game.current_player == 2 and not game.is_over():
move = game.get_move()
remove_coins(int(move))
game.play_move(move)
...
Teraz możemy już zagrać ze sztuczną inteligencją!
Pełna gra¶
import pgzrun
from gameOfCoins import GameOfCoins
from easyAI import *
import random
WIDTH = 840
HEIGHT = 600
game = None
dices = []
coins = []
win = 0
timer = 120
def draw():
screen.fill("white")
for die in dices:
die.draw()
for cn in coins:
cn.draw()
if win == 1:
screen.draw.text("You win!", center=(WIDTH / 2, HEIGHT / 3), color="blue", fontsize=120)
elif win == 2:
screen.draw.text("AI wins!", center=(WIDTH / 2, HEIGHT / 3), color="blue", fontsize=120)
elif timer > 0 and game.current_player == 2:
screen.draw.text("AI thinks...", center=(WIDTH / 2, HEIGHT - 200), color="red", fontsize=90)
elif game.current_player == 1:
screen.draw.text("Your move!", center=(WIDTH / 2, HEIGHT - 200), color="red", fontsize=90)
def update():
global win, timer
timer -= 1
if game.current_player == 2 and not game.is_over() and timer <= 0:
move = game.get_move()
remove_coins(int(move))
game.play_move(move)
if game.is_over():
win = 1
def on_mouse_down(pos):
global win, timer
for i in range(len(dices)):
if dices[i].collidepoint(pos) and game.current_player == 1:
move = game.possible_moves()[i]
if int(move) <= game.pile:
game.play_move(move)
remove_coins(int(move))
timer = random.randint(120, 260)
if game.is_over():
win = 2
def remove_coins(number):
for i in range(number):
coins.pop()
def init():
global game
algorithm = Negamax(13)
game = GameOfCoins([Human_Player(), AI_Player(algorithm)])
x = WIDTH / 2 - (len(game.possible_moves()) * 88 - 20) / 2 + 34
y = HEIGHT - 88
for move in game.possible_moves():
dices.append(Actor(f"dice{move}", (x, y)))
x += 68 + 20
x = 55
y = 50
for i in range(1, game.pile + 1):
coins.append(Actor("coin", (x, y)))
x += 84 + 20
if i % 8 == 0:
y += 84 + 30
x = 55
init()
pgzrun.go()