Skip to content

Commit 0c9164a

Browse files
Merge pull request #9 from pyscript/tic-tac-toe
add tic tac toe example
2 parents c81174d + 9e2f97b commit 0c9164a

File tree

7 files changed

+344
-0
lines changed

7 files changed

+344
-0
lines changed

tic-tac-toe/assets/css/examples.css

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
body {
2+
margin: 0;
3+
}
4+
5+
.pyscript {
6+
margin: 0.5rem;
7+
}
8+
9+
html {
10+
font-family:
11+
ui-sans-serif,
12+
system-ui,
13+
-apple-system,
14+
BlinkMacSystemFont,
15+
"Segoe UI",
16+
Roboto,
17+
"Helvetica Neue",
18+
Arial,
19+
"Noto Sans",
20+
sans-serif,
21+
"Apple Color Emoji",
22+
"Segoe UI Emoji",
23+
"Segoe UI Symbol",
24+
"Noto Color Emoji";
25+
line-height: 1.5;
26+
}
27+
28+
nav {
29+
position: sticky;
30+
width: 100%;
31+
top: 0;
32+
left: 0;
33+
z-index: 9999;
34+
}
35+
36+
.logo {
37+
padding-right: 10px;
38+
font-size: 28px;
39+
height: 30px;
40+
max-width: inherit;
41+
}
42+
43+
.title {
44+
text-decoration: none;
45+
text-decoration-line: none;
46+
text-decoration-style: initial;
47+
text-decoration-color: initial;
48+
font-weight: 400;
49+
font-size: 1.5em;
50+
line-height: 2em;
51+
white-space: nowrap;
52+
}
53+
54+
.app-header {
55+
display: flex;
56+
align-items: center;
57+
padding: 0.5rem 1rem;
58+
}

tic-tac-toe/assets/css/tictactoe.css

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
h1, h2 {
2+
font-family: 'Indie Flower', 'Comic Sans', cursive;
3+
text-align: center;
4+
}
5+
6+
#board {
7+
font-family: 'Indie Flower', 'Comic Sans', cursive;
8+
position: relative;
9+
font-size: 120px;
10+
margin: 1% auto;
11+
border-collapse: collapse;
12+
}
13+
#board td {
14+
border: 4px solid rgb(60, 60, 60);
15+
width: 90px;
16+
height: 90px;
17+
vertical-align: middle;
18+
text-align: center;
19+
cursor: pointer;
20+
}
21+
22+
#board td div {
23+
width: 90px;
24+
height: 90px;
25+
line-height: 90px;
26+
display: block;
27+
overflow: hidden;
28+
cursor: pointer;
29+
}
30+
31+
.x {
32+
color: darksalmon;
33+
position: relative;
34+
font-size: 1.2em;
35+
cursor: default;
36+
}
37+
.o {
38+
color: aquamarine;
39+
position: relative;
40+
font-size: 1.0em;
41+
cursor: default;
42+
}
43+
44+
.win {
45+
background-color: beige;
46+
}

tic-tac-toe/assets/favicon.png

4.19 KB
Loading

tic-tac-toe/assets/logo.png

7.27 KB
Loading

tic-tac-toe/index.html

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<!doctype html>
2+
3+
<html>
4+
<head>
5+
<!-- Recommended meta tags -->
6+
<meta charset="UTF-8">
7+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
8+
9+
<!-- PyScript CSS -->
10+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@pyscript/core/dist/core.css">
11+
<!-- CSS for examples -->
12+
<link rel="stylesheet" href="./assets/css/examples.css" />
13+
14+
<!-- This script tag bootstraps PyScript -->
15+
<script type="module" src="https://cdn.jsdelivr.net/npm/@pyscript/core/dist/core.js"></script>
16+
17+
<!-- Custom CSS -->
18+
<link href="https://fonts.googleapis.com/css?family=Indie+Flower" rel="stylesheet">
19+
<link rel="stylesheet" href="./assets/css/tictactoe.css" />
20+
21+
<!-- for splashscreen -->
22+
<style>
23+
#loading { outline: none; border: none; background: transparent }
24+
</style>
25+
<script type="module">
26+
const loading = document.getElementById('loading');
27+
addEventListener('py:ready', () => loading.close());
28+
loading.showModal();
29+
</script>
30+
31+
<title>Tic Tac Toe</title>
32+
<link rel="icon" type="image/png" href="./assets/favicon.png" />
33+
</head>
34+
35+
<body>
36+
<dialog id="loading">
37+
<h1>Loading...</h1>
38+
</dialog>
39+
40+
<nav class="navbar" style="background-color: #000000">
41+
<div class="app-header">
42+
<a href="/">
43+
<img src="./assets/logo.png" class="logo" />
44+
</a>
45+
<a class="title" href="" style="color: #f0ab3c">Tic Tac Toe</a>
46+
</div>
47+
</nav>
48+
49+
<section class="pyscript">
50+
<h1>Tic-Tac-Toe</h1>
51+
52+
<script type="py" src="./main.py" config="./pyscript.toml"></script>
53+
54+
<table id="board">
55+
<tr>
56+
<td><div id="cell00" data-x="0" data-y="0" class="cell" py-click="GAME.click"></div></td>
57+
<td><div id="cell01" data-x="0" data-y="1" class="cell" py-click="GAME.click"></div></td>
58+
<td><div id="cell02" data-x="0" data-y="2" class="cell" py-click="GAME.click"></div></td>
59+
<tr>
60+
<td><div id="cell10" data-x="1" data-y="0" class="cell" py-click="GAME.click"></div></td>
61+
<td><div id="cell11" data-x="1" data-y="1" class="cell" py-click="GAME.click"></div></td>
62+
<td><div id="cell12" data-x="1" data-y="2" class="cell" py-click="GAME.click"></div></td>
63+
</tr>
64+
<tr>
65+
<td><div id="cell20" data-x="2" data-y="0" class="cell" py-click="GAME.click"></div></td>
66+
<td><div id="cell21" data-x="2" data-y="1" class="cell" py-click="GAME.click"></div></td>
67+
<td><div id="cell22" data-x="2" data-y="2" class="cell" py-click="GAME.click"></div></td>
68+
</tr>
69+
</table>
70+
71+
<h2 id="status"></h2>
72+
<button id="btn-new-game" py-click="GAME.new_game">New game</button>
73+
<button id="btn-toggle-terminal" py-click="GAME.toggle_terminal">Hide/show terminal</button>
74+
75+
<div id="terminal" hidden="hidden">
76+
<script id="console" type="py" terminal></script>
77+
</div>
78+
79+
</section>
80+
</body>
81+
82+
</html>

tic-tac-toe/main.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
from pyweb import pydom
2+
3+
class TicTacToe:
4+
def __init__(self):
5+
self.board = pydom["table#board"]
6+
self.status = pydom["h2#status"]
7+
self.console = pydom["script#console"][0]
8+
self.init_cells()
9+
self.init_winning_combos()
10+
self.new_game(...)
11+
12+
def set_status(self, text):
13+
self.status.html = text
14+
15+
def init_cells(self):
16+
self.cells = []
17+
for i in (0, 1, 2):
18+
row = []
19+
for j in (0, 1, 2):
20+
cell = pydom[f"div#cell{i}{j}"][0]
21+
assert cell
22+
row.append(cell)
23+
self.cells.append(row)
24+
25+
def init_winning_combos(self):
26+
self.winning_combos = []
27+
# winning columns
28+
for i in (0, 1, 2):
29+
combo = []
30+
for j in (0, 1, 2):
31+
combo.append((i, j))
32+
self.winning_combos.append(combo)
33+
34+
# winning rows
35+
for j in (0, 1, 2):
36+
combo = []
37+
for i in (0, 1, 2):
38+
combo.append((i, j))
39+
self.winning_combos.append(combo)
40+
41+
# winning diagonals
42+
self.winning_combos.append([(0, 0), (1, 1), (2, 2)])
43+
self.winning_combos.append([(0, 2), (1, 1), (2, 0)])
44+
45+
def new_game(self, event):
46+
self.clear_terminal()
47+
print('=================')
48+
print('NEW GAME STARTING')
49+
print()
50+
for i in (0, 1, 2):
51+
for j in (0, 1, 2):
52+
self.set_cell(i, j, "")
53+
54+
self.current_player = "x"
55+
self.set_status(f'{self.current_player} playing...')
56+
57+
def next_turn(self):
58+
winner = self.check_winner()
59+
if winner == "tie":
60+
self.set_status("It's a tie!")
61+
self.current_player = "" # i.e., game ended
62+
return
63+
elif winner is not None:
64+
self.set_status(f'{winner} wins')
65+
self.current_player = "" # i.e., game ended
66+
return
67+
68+
if self.current_player == "x":
69+
self.current_player = "o"
70+
else:
71+
self.current_player = "x"
72+
self.set_status(f'{self.current_player} playing...')
73+
74+
def check_winner(self):
75+
"""
76+
Check whether the game as any winner.
77+
78+
Return "x", "o", "tie" or None. None means that the game is still playing.
79+
"""
80+
# check whether we have a winner
81+
for combo in self.winning_combos:
82+
winner = self.get_winner(combo)
83+
if winner:
84+
# highlight the winning cells
85+
for i, j in combo:
86+
self.cells[i][j].add_class("win")
87+
return winner
88+
89+
# check whether it's a tie
90+
for i in (0, 1, 2):
91+
for j in (0, 1, 2):
92+
if self.get_cell(i, j) == "":
93+
# there is at least an empty cell, it's not a tie
94+
return None # game still playing
95+
return "tie"
96+
97+
def get_winner(self, combo):
98+
"""
99+
If all the cells at the given points have the same value, return it.
100+
Else return "".
101+
102+
Each point is a tuple of (i, j) coordinates.
103+
Example:
104+
self.get_winner([(0, 0), (1, 1), (2, 2)])
105+
"""
106+
assert len(combo) == 3
107+
values = [self.get_cell(i, j) for i, j in combo]
108+
if values[0] == values[1] == values[2] and values[0] != "":
109+
return values[0]
110+
return ""
111+
112+
def set_cell(self, i, j, value):
113+
assert value in ("", "x", "o")
114+
cell = self.cells[i][j]
115+
cell.html = value
116+
if "x" in cell.classes:
117+
cell.remove_class("x")
118+
if "o" in cell.classes:
119+
cell.remove_class("o")
120+
if "win" in cell.classes:
121+
cell.remove_class("win")
122+
if value != "":
123+
cell.add_class(value)
124+
125+
def get_cell(self, i, j):
126+
cell = self.cells[i][j]
127+
value = cell.html
128+
assert value in ("", "x", "o")
129+
return value
130+
131+
def click(self, event):
132+
i = int(event.target.getAttribute('data-x'))
133+
j = int(event.target.getAttribute('data-y'))
134+
print(f'Cell {i}, {j} clicked: ', end='')
135+
if self.current_player == "":
136+
print('game ended, nothing to do')
137+
return
138+
#
139+
value = self.get_cell(i, j)
140+
if value == "":
141+
print('cell empty, setting it')
142+
self.set_cell(i, j, self.current_player)
143+
self.next_turn()
144+
else:
145+
print(f'cell already full, cannot set it')
146+
147+
def clear_terminal(self):
148+
self.console._js.terminal.clear()
149+
150+
def toggle_terminal(self, event):
151+
hidden = self.console.parent._js.getAttribute("hidden")
152+
if hidden:
153+
self.console.parent._js.removeAttribute("hidden")
154+
else:
155+
self.console.parent._js.setAttribute("hidden", "hidden")
156+
157+
GAME = TicTacToe()

tic-tac-toe/pyscript.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
name = "Tic Tac Toe"

0 commit comments

Comments
 (0)