ai-snake-lab 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ """
2
+ constants/DDef.py
3
+
4
+ AI Snake Game Simulator
5
+ Author: Nadim-Daniel Ghaznavi
6
+ Copyright: (c) 2024-2025 Nadim-Daniel Ghaznavi
7
+ GitHub: https://github.com/NadimGhaznavi/ai
8
+ License: GPL 3.0
9
+ """
10
+
11
+ from utils.ConstGroup import ConstGroup
12
+
13
+
14
+ class DDef(ConstGroup):
15
+ """Defaults"""
16
+
17
+ APP_TITLE: str = "AI Snake Game Simulator"
18
+ MOVE_DELAY: float = 0.0
@@ -0,0 +1,16 @@
1
+ """
2
+ constants/DDir.py
3
+
4
+ AI Snake Game Simulator
5
+ Author: Nadim-Daniel Ghaznavi
6
+ Copyright: (c) 2024-2025 Nadim-Daniel Ghaznavi
7
+ GitHub: https://github.com/NadimGhaznavi/ai
8
+ License: GPL 3.0
9
+ """
10
+
11
+ from utils.ConstGroup import ConstGroup
12
+
13
+
14
+ class DDir(ConstGroup):
15
+ """Directories"""
16
+ UTILS : str = "utils"
@@ -0,0 +1,19 @@
1
+ """
2
+ constants/DEpsilon.py
3
+
4
+ AI Snake Game Simulator
5
+ Author: Nadim-Daniel Ghaznavi
6
+ Copyright: (c) 2024-2025 Nadim-Daniel Ghaznavi
7
+ GitHub: https://github.com/NadimGhaznavi/ai
8
+ License: GPL 3.0
9
+ """
10
+
11
+ from utils.ConstGroup import ConstGroup
12
+
13
+
14
+ class DEpsilon(ConstGroup):
15
+ """Epsilon Defaults"""
16
+
17
+ EPSILON_INITIAL: float = 0.99
18
+ EPSILON_MIN: float = 0.0
19
+ EPSILON_DECAY: float = 0.95
@@ -0,0 +1,18 @@
1
+ """
2
+ constants/DFields.py
3
+
4
+ AI Snake Game Simulator
5
+ Author: Nadim-Daniel Ghaznavi
6
+ Copyright: (c) 2024-2025 Nadim-Daniel Ghaznavi
7
+ GitHub: https://github.com/NadimGhaznavi/ai
8
+ License: GPL 3.0
9
+ """
10
+
11
+ from utils.ConstGroup import ConstGroup
12
+
13
+
14
+ class DField(ConstGroup):
15
+ """Fields"""
16
+
17
+ RUNNING: str = "running"
18
+ STOPPED: str = "stopped"
@@ -0,0 +1,17 @@
1
+ """
2
+ constants/DFile.py
3
+
4
+ AI Snake Game Simulator
5
+ Author: Nadim-Daniel Ghaznavi
6
+ Copyright: (c) 2024-2025 Nadim-Daniel Ghaznavi
7
+ GitHub: https://github.com/NadimGhaznavi/ai
8
+ License: GPL 3.0
9
+ """
10
+
11
+ from utils.ConstGroup import ConstGroup
12
+
13
+
14
+ class DFile(ConstGroup):
15
+ """Files"""
16
+
17
+ CSS_PATH: str = "AISim.tcss"
@@ -0,0 +1,34 @@
1
+ """
2
+ constants/DLabels.py
3
+
4
+ AI Snake Game Simulator
5
+ Author: Nadim-Daniel Ghaznavi
6
+ Copyright: (c) 2024-2025 Nadim-Daniel Ghaznavi
7
+ GitHub: https://github.com/NadimGhaznavi/ai
8
+ License: GPL 3.0
9
+ """
10
+
11
+ from utils.ConstGroup import ConstGroup
12
+
13
+
14
+ class DLabel(ConstGroup):
15
+ """Labels"""
16
+
17
+ EPSILON: str = "Epsilon"
18
+ EPSILON_DECAY: str = "Epsilon Decay"
19
+ EPSILON_INITIAL: str = "Initial Epsilon"
20
+ EPSILON_MIN: str = "Minimum Epsilon"
21
+ GAME: str = "Game"
22
+ HIGHSCORE: str = "Highscore"
23
+ MEM_TYPE: str = "Memory Type"
24
+ MEMORIES: str = "Memories"
25
+ MIN_EPSILON: str = "Minimum Epsilon"
26
+ MOVE_DELAY: str = "Move Delay"
27
+ PAUSE: str = "Pause"
28
+ QUIT: str = "Quit"
29
+ RUNTIME: str = "Runtime Values"
30
+ SCORE: str = "Score"
31
+ SETTINGS: str = "Configuration Settings"
32
+ START: str = "Start"
33
+ RESET: str = "Reset"
34
+ UPDATE: str = "Update"
@@ -0,0 +1,39 @@
1
+ """
2
+ constants/DLayout.py
3
+
4
+ AI Snake Game Simulator
5
+ Author: Nadim-Daniel Ghaznavi
6
+ Copyright: (c) 2024-2025 Nadim-Daniel Ghaznavi
7
+ GitHub: https://github.com/NadimGhaznavi/ai
8
+ License: GPL 3.0
9
+ """
10
+
11
+ from utils.ConstGroup import ConstGroup
12
+
13
+
14
+ class DLayout(ConstGroup):
15
+ """Layout"""
16
+
17
+ BUTTON_PAUSE: str = "button_pause"
18
+ BUTTON_QUIT: str = "button_quit"
19
+ BUTTON_ROW: str = "button_row"
20
+ BUTTON_START: str = "button_start"
21
+ BUTTON_RESET: str = "button_reset"
22
+ BUTTON_UPDATE: str = "button_update"
23
+ CUR_EPSILON: str = "cur_epsilon"
24
+ CUR_MEM_TYPE: str = "cur_mem_type"
25
+ GAME_BOARD: str = "game_board"
26
+ GAME_BOX: str = "game_box"
27
+ EPSILON_DECAY: str = "epsilon_decay"
28
+ EPSILON_INITIAL: str = "initial_epsilon"
29
+ EPSILON_MIN: str = "epsilon_min"
30
+ INPUT_10: str = "input_10"
31
+ LABEL: str = "label"
32
+ LABEL_SETTINGS: str = "label_settings"
33
+ MOVE_DELAY: str = "move_delay"
34
+ NUM_MEMORIES: str = "num_memories"
35
+ RUNTIME_BOX: str = "runtime_box"
36
+ SCORE: str = "score"
37
+ SETTINGS_BOX: str = "settings_box"
38
+ TITLE: str = "title"
39
+ VARIABLE: str = "variable"
@@ -0,0 +1,17 @@
1
+ """
2
+ constants/DModelL.py
3
+
4
+ AI Snake Game Simulator
5
+ Author: Nadim-Daniel Ghaznavi
6
+ Copyright: (c) 2024-2025 Nadim-Daniel Ghaznavi
7
+ GitHub: https://github.com/NadimGhaznavi/ai
8
+ License: GPL 3.0
9
+ """
10
+
11
+ from utils.ConstGroup import ConstGroup
12
+
13
+
14
+ class DModelL(ConstGroup):
15
+ """Linear Model Defaults"""
16
+
17
+ LEARNING_RATE: float = 0.000009
@@ -0,0 +1,20 @@
1
+ """
2
+ constants/DModelRNN.py
3
+
4
+ AI Snake Game Simulator
5
+ Author: Nadim-Daniel Ghaznavi
6
+ Copyright: (c) 2024-2025 Nadim-Daniel Ghaznavi
7
+ GitHub: https://github.com/NadimGhaznavi/ai
8
+ License: GPL 3.0
9
+ """
10
+
11
+ from utils.ConstGroup import ConstGroup
12
+
13
+
14
+ class DModelRNN(ConstGroup):
15
+ """RNN Model Defaults"""
16
+
17
+ LEARNING_RATE: float = 0.0007
18
+ INPUT_SIZE: int = 400
19
+ MAX_MEMORIES: int = 20
20
+ MAX_MEMORY: int = 100000
@@ -0,0 +1,25 @@
1
+ """
2
+ constants/DReplayMemory.py
3
+
4
+ AI Snake Game Simulator
5
+ Author: Nadim-Daniel Ghaznavi
6
+ Copyright: (c) 2024-2025 Nadim-Daniel Ghaznavi
7
+ GitHub: https://github.com/NadimGhaznavi/ai
8
+ License: GPL 3.0
9
+ """
10
+
11
+ from utils.ConstGroup import ConstGroup
12
+
13
+
14
+ class MEM_TYPE(ConstGroup):
15
+ """Replay Memory Type"""
16
+
17
+ SHUFFLE: str = "shuffle"
18
+ SHUFFLE_LABEL: str = "Shuffled set"
19
+ RANDOM_GAME: str = "random_game"
20
+ RANDOM_GAME_LABEL: str = "Random game"
21
+
22
+ MEM_TYPE_TABLE: dict = {
23
+ SHUFFLE: SHUFFLE_LABEL,
24
+ RANDOM_GAME: RANDOM_GAME_LABEL,
25
+ }
File without changes
@@ -0,0 +1,221 @@
1
+ """
2
+ game/AISim.py
3
+
4
+ AI Snake Game Simulator
5
+ Author: Nadim-Daniel Ghaznavi
6
+ Copyright: (c) 2024-2025 Nadim-Daniel Ghaznavi
7
+ GitHub: https://github.com/NadimGhaznavi/ai
8
+ License: GPL 3.0
9
+ """
10
+
11
+ import numpy as np
12
+
13
+ from textual.scroll_view import ScrollView
14
+ from textual.geometry import Offset, Region, Size
15
+ from textual.strip import Strip
16
+ from textual.reactive import var
17
+
18
+ from rich.segment import Segment
19
+ from rich.style import Style
20
+
21
+ from game.GameElements import Direction
22
+
23
+ emptyA = "#111111"
24
+ emptyB = "#000000"
25
+ food = "#940101"
26
+ snake = "#025b02"
27
+ snake_head = "#16e116"
28
+
29
+
30
+ class GameBoard(ScrollView):
31
+ COMPONENT_CLASSES = {
32
+ "gameboard--emptyA-square",
33
+ "gameboard--emptyB-square",
34
+ "gameboard--food-square",
35
+ "gameboard--snake-square",
36
+ "gameboard--snake-head-square",
37
+ }
38
+
39
+ DEFAULT_CSS = """
40
+ GameBoard > .gameboard--emptyA-square {
41
+ background: #111111;
42
+ }
43
+ GameBoard > .gameboard--emptyB-square {
44
+ background: #000000;
45
+ }
46
+ GameBoard > .gameboard--food-square {
47
+ background: #940101;
48
+ }
49
+ GameBoard > .gameboard--snake-square {
50
+ background: #025b02;
51
+ }
52
+ GameBoard > .gameboard--snake-head-square {
53
+ background: #0ca30c;
54
+ }
55
+ """
56
+
57
+ food = var(Offset(0, 0))
58
+ snake_head = var(Offset(0, 0))
59
+ snake_body = var([])
60
+ direction = Direction.RIGHT
61
+ last_dirs = [0, 0, 1, 0]
62
+
63
+ def __init__(self, board_size: int, id=None) -> None:
64
+ super().__init__(id=id)
65
+ self._board_size = board_size
66
+ self.virtual_size = Size(board_size * 2, board_size)
67
+
68
+ def board_size(self) -> int:
69
+ return self._board_size
70
+
71
+ def get_state(self):
72
+
73
+ head = self.snake_head
74
+ direction = self.direction
75
+ point_l = Offset(head.x - 1, head.y)
76
+ point_r = Offset(head.x + 1, head.y)
77
+ point_u = Offset(head.x, head.y - 1)
78
+ point_d = Offset(head.x, head.y + 1)
79
+ dir_l = direction == Direction.LEFT
80
+ dir_r = direction == Direction.RIGHT
81
+ dir_u = direction == Direction.UP
82
+ dir_d = direction == Direction.DOWN
83
+
84
+ state = [
85
+ # 1. Snake collision straight ahead
86
+ (dir_r and self.is_snake_collision(point_r))
87
+ or (dir_l and self.is_snake_collision(point_l))
88
+ or (dir_u and self.is_snake_collision(point_u))
89
+ or (dir_d and self.is_snake_collision(point_d)),
90
+ # 2. Snake collision to the right
91
+ (dir_u and self.is_snake_collision(point_r))
92
+ or (dir_d and self.is_snake_collision(point_l))
93
+ or (dir_l and self.is_snake_collision(point_u))
94
+ or (dir_r and self.is_snake_collision(point_d)),
95
+ # 3. Snake collision to the left
96
+ (dir_d and self.is_snake_collision(point_r))
97
+ or (dir_u and self.is_snake_collision(point_l))
98
+ or (dir_r and self.is_snake_collision(point_u))
99
+ or (dir_l and self.is_snake_collision(point_d)),
100
+ # 4. divider
101
+ 0,
102
+ # 5. Wall collision straight ahead
103
+ (dir_r and self.is_wall_collision(point_r))
104
+ or (dir_l and self.is_wall_collision(point_l))
105
+ or (dir_u and self.is_wall_collision(point_u))
106
+ or (dir_d and self.is_wall_collision(point_d)),
107
+ # 6. Wall collision to the right
108
+ (dir_u and self.is_wall_collision(point_r))
109
+ or (dir_d and self.is_wall_collision(point_l))
110
+ or (dir_l and self.is_wall_collision(point_u))
111
+ or (dir_r and self.is_wall_collision(point_d)),
112
+ # 7. Wall collision to the left
113
+ (dir_d and self.is_wall_collision(point_r))
114
+ or (dir_u and self.is_wall_collision(point_l))
115
+ or (dir_r and self.is_wall_collision(point_u))
116
+ or (dir_l and self.is_wall_collision(point_d)),
117
+ # 8. divider
118
+ 0,
119
+ # 9, 10, 11, 12. Last move direction
120
+ dir_l,
121
+ dir_r,
122
+ dir_u,
123
+ dir_d,
124
+ # 13. divider
125
+ 0,
126
+ # 14 - 23. Food location
127
+ self.food.x < self.snake_head.x, # Food left
128
+ self.food.x > self.snake_head.x, # Food right
129
+ self.food.y < self.snake_head.y, # Food up
130
+ self.food.y > self.snake_head.y, # Food down
131
+ self.food.x == self.snake_head.x,
132
+ self.food.x == self.snake_head.x
133
+ and self.food.y > self.snake_head.y, # Food ahead
134
+ self.food.x == self.snake_head.x
135
+ and self.food.y < self.snake_head.y, # Food behind
136
+ self.food.y == self.snake_head.y,
137
+ self.food.y == self.snake_head.y
138
+ and self.food.x > self.snake_head.x, # Food above
139
+ self.food.y == self.snake_head.y
140
+ and self.food.x < self.snake_head.x, # Food below
141
+ ]
142
+
143
+ # 24, 25, 26 and 27. Previous direction of the snake
144
+ for aDir in self.last_dirs:
145
+ state.append(int(aDir))
146
+ self.last_dirs = [dir_l, dir_r, dir_u, dir_d]
147
+
148
+ return np.array(state, dtype="int8")
149
+
150
+ def is_snake_collision(self, pt: Offset) -> bool:
151
+ if pt in self.snake_body:
152
+ return True
153
+ return False
154
+
155
+ def is_wall_collision(self, pt: Offset) -> bool:
156
+ if pt.x >= self._board_size or pt.x < 0 or pt.y >= self._board_size or pt.y < 0:
157
+ return True
158
+ return False
159
+
160
+ def render_line(self, y: int) -> Strip:
161
+ scroll_x, scroll_y = self.scroll_offset
162
+ y += scroll_y
163
+ row_index = y
164
+
165
+ emptyA = self.get_component_rich_style("gameboard--emptyA-square")
166
+ emptyB = self.get_component_rich_style("gameboard--emptyB-square")
167
+ food = self.get_component_rich_style("gameboard--food-square")
168
+ snake = self.get_component_rich_style("gameboard--snake-square")
169
+ snake_head = self.get_component_rich_style("gameboard--snake-head-square")
170
+
171
+ if row_index >= self._board_size:
172
+ return Strip.blank(self.size.width)
173
+
174
+ is_odd = row_index % 2
175
+
176
+ def get_square_style(column: int, row: int) -> Style:
177
+ if self.food == Offset(column, row):
178
+ square_style = food
179
+ elif self.snake_head == Offset(column, row):
180
+ square_style = snake_head
181
+ elif Offset(column, row) in self.snake_body:
182
+ square_style = snake
183
+ else:
184
+ square_style = emptyA if (column + is_odd) % 2 else emptyB
185
+ return square_style
186
+
187
+ segments = [
188
+ Segment(" " * 2, get_square_style(column, row_index))
189
+ for column in range(self._board_size)
190
+ ]
191
+ strip = Strip(segments, self._board_size * 2)
192
+ # Crop the strip so that is covers the visible area
193
+ strip = strip.crop(scroll_x, scroll_x + self.size.width)
194
+ return strip
195
+
196
+ def watch_food(self, previous_food: Offset, food: Offset) -> None:
197
+ """Called when the food square changes."""
198
+
199
+ def get_square_region(square_offset: Offset) -> Region:
200
+ """Get region relative to widget from square coordinate."""
201
+ x, y = square_offset
202
+ region = Region(x * 2, y, 2, 1)
203
+ # Move the region into the widgets frame of reference
204
+ region = region.translate(-self.scroll_offset)
205
+ return region
206
+
207
+ # Refresh the previous food square
208
+ self.refresh(get_square_region(previous_food))
209
+
210
+ # Refresh the new food square
211
+ self.refresh(get_square_region(food))
212
+
213
+ def update_food(self, food: Offset) -> None:
214
+ self.food = food
215
+ self.refresh()
216
+
217
+ def update_snake(self, snake: list[Offset], direction: Direction) -> None:
218
+ self.direction = direction
219
+ self.snake_head = snake[0]
220
+ self.snake_body = snake[1:]
221
+ self.refresh()
@@ -0,0 +1,27 @@
1
+ """
2
+ game/GameElements.py
3
+
4
+ AI Snake Game Simulator
5
+ Author: Nadim-Daniel Ghaznavi
6
+ Copyright: (c) 2024-2025 Nadim-Daniel Ghaznavi
7
+ GitHub: https://github.com/NadimGhaznavi/ai
8
+ License: GPL 3.0
9
+ """
10
+
11
+ from enum import Enum
12
+
13
+
14
+ class Direction(Enum):
15
+ """
16
+ A simple Enum class that represents a direction in the
17
+ Snake game. It has four values:
18
+ 1. RIGHT
19
+ 2. LEFT
20
+ 3. UP
21
+ 4. DOWN
22
+ """
23
+
24
+ RIGHT = 1
25
+ LEFT = 2
26
+ UP = 3
27
+ DOWN = 4
@@ -0,0 +1,178 @@
1
+ """
2
+ game/SnakeGame.py
3
+
4
+ AI Snake Game Simulator
5
+ Author: Nadim-Daniel Ghaznavi
6
+ Copyright: (c) 2024-2025 Nadim-Daniel Ghaznavi
7
+ GitHub: https://github.com/NadimGhaznavi/ai
8
+ License: GPL 3.0
9
+ """
10
+
11
+ import time
12
+ import random
13
+ import numpy as np
14
+
15
+ from textual.geometry import Offset
16
+
17
+ from game.GameBoard import GameBoard
18
+ from game.GameElements import Direction
19
+
20
+ # Maximum number of moves. This is multiplied by the length of the snake. The game
21
+ # ends if game moves > MAX_MOVES * length-of-snake. This avoids enless AI looping behavior.
22
+ MAX_MOVES = 100
23
+
24
+
25
+ class SnakeGame:
26
+
27
+ def __init__(self, game_board: GameBoard, id=None):
28
+ # Make multiple runs predictable
29
+ random.seed(1970)
30
+
31
+ # Get an instance of the game board and it's dimensions
32
+ self.game_board = game_board
33
+ board_size = self.game_board.board_size()
34
+
35
+ # Track the number of moves
36
+ self.moves = 0
37
+
38
+ # Track the reward within a given game
39
+ self.game_reward = 0
40
+
41
+ # Set the initial snake direction and position
42
+ self.direction = Direction.RIGHT
43
+ self.head = Offset(board_size // 2, board_size // 2)
44
+ self.snake = [
45
+ self.head,
46
+ Offset(self.head.x - 1, self.head.y),
47
+ Offset(self.head.x - 2, self.head.y),
48
+ ]
49
+
50
+ # Place a food in a random location (not occupied by the snake)
51
+ self.food = self.place_food()
52
+
53
+ # Update the game board
54
+ self.game_board.update_snake(snake=self.snake, direction=self.direction)
55
+ self.game_board.update_food(food=self.food)
56
+
57
+ # The current game score
58
+ self.game_score = 0
59
+
60
+ def get_direction(self):
61
+ return self.direction
62
+
63
+ def move(self, action):
64
+ clock_wise = [Direction.RIGHT, Direction.DOWN, Direction.LEFT, Direction.UP]
65
+ idx = clock_wise.index(self.direction)
66
+ if np.array_equal(action, [1, 0, 0]):
67
+ new_dir = clock_wise[idx] # no change
68
+ elif np.array_equal(action, [0, 1, 0]):
69
+ next_idx = (idx + 1) % 4 # Mod 4 to avoid out of index error
70
+ new_dir = clock_wise[next_idx] # right turn r -> d -> l -> u
71
+ else: # [0, 0, 1] ... there are only 3 actions
72
+ next_idx = (idx - 1) % 4 # Again, MOD 4 to avoid out of index error
73
+ new_dir = clock_wise[next_idx] # left turn r -> u -> l -> d
74
+ self.direction = new_dir
75
+
76
+ old_head = self.head
77
+
78
+ if self.direction == Direction.RIGHT:
79
+ self.head = Offset(self.head.x + 1, self.head.y)
80
+ elif self.direction == Direction.LEFT:
81
+ self.head = Offset(self.head.x - 1, self.head.y)
82
+ elif self.direction == Direction.DOWN:
83
+ self.head = Offset(self.head.x, self.head.y + 1)
84
+ elif self.direction == Direction.UP:
85
+ self.head = Offset(self.head.x, self.head.y - 1)
86
+
87
+ # self.snake.insert(0, self.head)
88
+
89
+ def place_food(self):
90
+ board_size = self.game_board.board_size()
91
+ x = random.randint(0, board_size - 1)
92
+ y = random.randint(0, board_size - 1)
93
+ self.food = Offset(x, y)
94
+ if self.food in self.snake:
95
+ self.place_food()
96
+ return self.food
97
+
98
+ def play_step(self, action):
99
+ ## 1. Move
100
+ self.move(action)
101
+ self.snake.insert(0, self.head)
102
+ self.game_board.update_snake(snake=self.snake, direction=self.direction)
103
+ snake_length = len(self.snake)
104
+ max_moves = 100
105
+
106
+ ## 2. Check if the game is over
107
+ reward = 0
108
+ game_over = False
109
+ board_size = self.game_board.board_size()
110
+
111
+ ## 3. Check for "game over" states
112
+
113
+ # Wall collision
114
+ if self.game_board.is_wall_collision(self.head):
115
+ # Wall collision
116
+ game_over = True
117
+ reward = -10
118
+
119
+ # Snake collision
120
+ elif self.game_board.is_snake_collision(self.head):
121
+ game_over = True
122
+ reward = -10
123
+
124
+ # Exceeded max moves
125
+ if self.moves > max_moves * snake_length:
126
+ game_over = True
127
+ reward = -10
128
+
129
+ if game_over == True:
130
+ # Game is over: Snake or wall collision or exceeded max moves
131
+ self.game_reward += reward
132
+ return reward, game_over, self.game_score
133
+
134
+ ## 4. Game is not over, lets see what else is going on
135
+
136
+ if self.head == self.food:
137
+ # We found food!!
138
+ self.game_reward += 10
139
+ self.place_food()
140
+ self.game_score += 1
141
+
142
+ else:
143
+ self.snake.pop()
144
+
145
+ self.game_reward += reward
146
+ self.game_board.update_snake(snake=self.snake, direction=self.direction)
147
+ self.game_board.update_food(food=self.food)
148
+ self.moves += 1
149
+ return reward, game_over, self.game_score
150
+
151
+ def reset(self):
152
+ # Reset the game reward
153
+ self.game_reward = 0
154
+
155
+ # Get the board size
156
+ board_size = self.game_board.board_size()
157
+
158
+ # Reset the number of moves
159
+ self.moves = 0
160
+
161
+ # Set the initial snake direction and position
162
+ self.direction = Direction.RIGHT
163
+ self.head = Offset(board_size // 2, board_size // 2)
164
+ self.snake = [
165
+ self.head,
166
+ Offset(self.head.x - 1, self.head.y),
167
+ Offset(self.head.x - 2, self.head.y),
168
+ ]
169
+
170
+ # Place a food in a random location (not occupied by the snake)
171
+ self.food = self.place_food()
172
+
173
+ # Update the game board
174
+ self.game_board.update_snake(snake=self.snake, direction=self.direction)
175
+ self.game_board.update_food(food=self.food)
176
+
177
+ # The current game score
178
+ self.game_score = 0