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.
- ai_snake_lab/AISim.py +274 -0
- ai_snake_lab/ai/AIAgent.py +84 -0
- ai_snake_lab/ai/AITrainer.py +90 -0
- ai_snake_lab/ai/EpsilonAlgo.py +73 -0
- ai_snake_lab/ai/ReplayMemory.py +90 -0
- ai_snake_lab/ai/models/ModelL.py +40 -0
- ai_snake_lab/ai/models/ModelRNN.py +43 -0
- ai_snake_lab/constants/DDef.py +18 -0
- ai_snake_lab/constants/DDir.py +16 -0
- ai_snake_lab/constants/DEpsilon.py +19 -0
- ai_snake_lab/constants/DFields.py +18 -0
- ai_snake_lab/constants/DFile.py +17 -0
- ai_snake_lab/constants/DLabels.py +34 -0
- ai_snake_lab/constants/DLayout.py +39 -0
- ai_snake_lab/constants/DModelL.py +17 -0
- ai_snake_lab/constants/DModelLRNN.py +20 -0
- ai_snake_lab/constants/DReplayMemory.py +25 -0
- ai_snake_lab/constants/__init__.py +0 -0
- ai_snake_lab/game/GameBoard.py +221 -0
- ai_snake_lab/game/GameElements.py +27 -0
- ai_snake_lab/game/SnakeGame.py +178 -0
- ai_snake_lab/utils/AISim.tcss +115 -0
- ai_snake_lab/utils/ConstGroup.py +49 -0
- ai_snake_lab-0.1.0.dist-info/LICENSE +674 -0
- ai_snake_lab-0.1.0.dist-info/METADATA +70 -0
- ai_snake_lab-0.1.0.dist-info/RECORD +28 -0
- ai_snake_lab-0.1.0.dist-info/WHEEL +4 -0
- ai_snake_lab-0.1.0.dist-info/entry_points.txt +3 -0
@@ -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
|