Skip to content

Commit 3607505

Browse files
authored
Merge branch 'master' into python-web-scraping-practical-introduction
2 parents 62f61cc + 043e2a5 commit 3607505

File tree

97 files changed

+34918
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+34918
-0
lines changed

tic-tac-toe-ai-python/README.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Build a Tic-Tac-Toe Game Engine With an AI Player in Python
2+
3+
This is a companion project to a Real Python [tutorial](https://realpython.com/tic-tac-toe-ai-python/) about implementing a game engine using the minimax algorithm as rudimentary artificial intelligence. It consists of two elements:
4+
5+
1. Game Engine Library
6+
2. Game Front Ends
7+
8+
---
9+
10+
## Game Engine Library
11+
12+
The underlying game logic is encapsulated in a common library, which can be reused across multiple [game front ends](#game-front-ends) without duplicating the code.
13+
14+
### Installing
15+
16+
Before proceeding, make sure you've created a virtual environment, activated it, and installed the tic-tac-toe game engine library into it:
17+
18+
```shell
19+
$ cd tic-tac-toe/
20+
$ python -m venv venv/
21+
$ source venv/bin/activate
22+
(venv) $ python -m pip install library/
23+
```
24+
25+
This will let you test the game front ends provided by this project.
26+
27+
### Packaging
28+
29+
One of the available game front ends relies on the library distributed as a Python wheel. Therefore, you must build and package the library accordingly:
30+
31+
```shell
32+
$ cd tic-tac-toe/
33+
$ python -m pip wheel library/
34+
$ mv tic_tac_toe-1.0.0-py3-none-any.whl frontends/browser/
35+
```
36+
37+
Note that you don't need a virtual environment for these commands to work, but running them in one is completely fine.
38+
39+
## Game Front Ends
40+
41+
There are a few game front ends implemented in separate packages for you to try out. Before running them, make sure you've followed the earlier steps just described. Now, change the directory to the game front ends parent folder:
42+
43+
```shell
44+
$ cd tic-tac-toe/frontends/
45+
```
46+
47+
### Browser Front End
48+
49+
Play tic-tac-toe in your web browser through PyScript:
50+
51+
```shell
52+
$ python -m browser
53+
```
54+
55+
This will start a local HTTP server and open the hosted HTML file in your web browser. Note that you don't need to create a virtual environment or install the game engine library to play the game because it's loaded dynamically from a Python wheel file.
56+
57+
Sample gameplay:
58+
59+
![](docs/browser.gif)
60+
61+
### Console Front End
62+
63+
Play tic-tac-toe in the terminal:
64+
65+
```shell
66+
(venv) $ python -m console
67+
```
68+
69+
You can optionally set one or both players to a human player (`human`), a computer player making random moves (`random`), or an unbeatable minimax computer player (`minimax`), as well as change the starting player:
70+
71+
```shell
72+
(venv) $ python -m console -X minimax -O random --starting O
73+
```
74+
75+
Sample gameplay:
76+
77+
![](docs/console.gif)
78+
79+
### Window Front End
80+
81+
Play tic-tac-toe against a minimax computer player in a GUI application built with Tkinter:
82+
83+
```shell
84+
(venv) $ python -m window
85+
```
86+
87+
To change the players, who are currently hard-coded, you'll need to edit the following fragment of the front end's code:
88+
89+
```python
90+
def game_loop(window: Window, events: Queue) -> None:
91+
player1 = WindowPlayer(Mark("X"), events)
92+
player2 = MinimaxComputerPlayer(Mark("O"))
93+
starting_mark = Mark("X")
94+
TicTacToe(player1, player2, WindowRenderer(window)).play(starting_mark)
95+
```
96+
97+
Sample gameplay:
98+
99+
![](docs/window.gif)
214 KB
Loading
44 KB
Loading
38.2 KB
Loading

tic-tac-toe-ai-python/source_code_bonus/tic-tac-toe/frontends/browser/__init__.py

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .cli import main
2+
3+
main()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import http.server
2+
import socketserver
3+
import threading
4+
import webbrowser
5+
6+
PORT = 8000
7+
8+
9+
def run_server(port: int = PORT) -> None:
10+
handler = http.server.SimpleHTTPRequestHandler
11+
with socketserver.TCPServer(("", port), handler) as httpd:
12+
print(f"Serving HTTP on http://127.0.0.1:{port}/ ...")
13+
httpd.serve_forever()
14+
15+
16+
def open_browser(port: int = PORT) -> None:
17+
webbrowser.open(f"http://127.0.0.1:{port}/browser/index.html")
18+
19+
20+
def main() -> None:
21+
threading.Thread(target=open_browser).start()
22+
run_server()
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Tic-Tac-Toe</title>
7+
<link rel="preconnect" href="https://fonts.googleapis.com">
8+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9+
<link href="https://fonts.googleapis.com/css2?family=Indie+Flower&display=swap" rel="stylesheet">
10+
<script defer src="https://pyscript.net/alpha/pyscript.js"></script>
11+
<style>
12+
.board {
13+
stroke: gray;
14+
stroke-width: 3;
15+
}
16+
.text {
17+
stroke: black;
18+
fill: black;
19+
font-size: 80px;
20+
font-family: 'Indie Flower', cursive;
21+
}
22+
.win {
23+
stroke: firebrick;
24+
fill: firebrick;
25+
}
26+
.hidden {
27+
display: none;
28+
}
29+
svg {
30+
margin: 20px;
31+
}
32+
</style>
33+
</head>
34+
<body>
35+
<div>
36+
<label>
37+
Player X:
38+
<select id="playerX" disabled>
39+
<option value="random">Random</option>
40+
<option value="minimax">Minimax</option>
41+
<option value="human" selected>Human</option>
42+
</select>
43+
</label>
44+
<label>
45+
Player O:
46+
<select id="playerO" disabled>
47+
<option value="random">Random</option>
48+
<option value="minimax" selected>Minimax</option>
49+
<option value="human">Human</option>
50+
</select>
51+
</label>
52+
</div>
53+
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
54+
<line x1="85" y1="0" x2="85" y2="255" class="board"/>
55+
<line x1="170" y1="0" x2="170" y2="255" class="board" />
56+
<line x1="0" y1="85" x2="255" y2="85" class="board" />
57+
<line x1="0" y1="170" x2="255" y2="170" class="board" />
58+
<a href="" data-id="0"><text x="20" y="70" class="text"></text></a>
59+
<a href="" data-id="1"><text x="105" y="70" class="text"></text></a>
60+
<a href="" data-id="2"><text x="190" y="70" class="text"></text></a>
61+
<a href="" data-id="3"><text x="20" y="155" class="text"></text></a>
62+
<a href="" data-id="4"><text x="105" y="155" class="text"></text></a>
63+
<a href="" data-id="5"><text x="190" y="155" class="text"></text></a>
64+
<a href="" data-id="6"><text x="20" y="240" class="text"></text></a>
65+
<a href="" data-id="7"><text x="105" y="240" class="text"></text></a>
66+
<a href="" data-id="8"><text x="190" y="240" class="text"></text></a>
67+
</svg>
68+
<h3 id="status"></h3>
69+
<button id="replay" class="hidden">Replay?</button>
70+
<py-env>
71+
- tic_tac_toe-1.0.0-py3-none-any.whl
72+
- paths:
73+
- players.py
74+
- renderers.py
75+
</py-env>
76+
<py-script src="script.py"></py-script>
77+
</body>
78+
</html>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from asyncio import Queue
2+
3+
from tic_tac_toe.game.players_async import AsyncPlayer
4+
from tic_tac_toe.logic.models import GameState, Mark, Move
5+
6+
7+
class BrowserPlayer(AsyncPlayer):
8+
def __init__(self, mark: Mark, events: Queue) -> None:
9+
super().__init__(mark)
10+
self.events = events
11+
12+
async def get_move(self, game_state: GameState) -> Move | None:
13+
index = await self.events.get()
14+
try:
15+
return game_state.make_move_to(index)
16+
finally:
17+
self.events.task_done()
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from js import document
2+
from tic_tac_toe.game.renderers import Renderer
3+
from tic_tac_toe.logic.models import GameState
4+
5+
6+
class BrowserRenderer(Renderer):
7+
def render(self, game_state: GameState) -> None:
8+
for i, cell in enumerate(game_state.grid.cells):
9+
button = document.querySelector(f"[data-id='{i}'] text")
10+
button.classList.remove("win")
11+
button.innerHTML = "&nbsp;" if cell == " " else cell
12+
status = document.querySelector("#status")
13+
if game_state.game_over:
14+
document.querySelector("#replay").classList.remove("hidden")
15+
for select in document.querySelectorAll("select"):
16+
select.removeAttribute("disabled")
17+
if game_state.winner:
18+
status.innerHTML = f"{game_state.winner} wins \N{party popper}"
19+
for i in game_state.winning_cells:
20+
button = document.querySelector(f"[data-id='{i}'] text")
21+
button.classList.add("win")
22+
elif game_state.tie:
23+
status.innerHTML = "Tie \N{neutral face}"
24+
else:
25+
document.querySelector("#status").innerHTML = ""

0 commit comments

Comments
 (0)