79 lines
3.0 KiB
Python
79 lines
3.0 KiB
Python
import numpy as np
|
|
from maze import get_walls, is_passable, NORTH, SOUTH, EAST, WEST
|
|
from config import MazeConfig
|
|
|
|
class NeuralNet:
|
|
WEIGHT_COUNT = 8*12 + 12 + 12*4 + 4 # = 160
|
|
|
|
def __init__(self, weights=None):
|
|
if weights is None:
|
|
weights = np.random.randn(self.WEIGHT_COUNT) * 0.5
|
|
self.weights = weights.astype(np.float32)
|
|
|
|
def forward(self, x: np.ndarray) -> int:
|
|
# x shape: (8,) float32
|
|
# Slices: W1=weights[0:96].reshape(8,12), b1=weights[96:108]
|
|
# W2=weights[108:156].reshape(12,4), b2=weights[156:160]
|
|
# Forward: h = ReLU(x @ W1 + b1), logits = h @ W2 + b2
|
|
# Return argmax(logits) — deterministic, no sampling
|
|
W1 = self.weights[0:96].reshape(8, 12)
|
|
b1 = self.weights[96:108]
|
|
W2 = self.weights[108:156].reshape(12, 4)
|
|
b2 = self.weights[156:160]
|
|
h = np.maximum(0.0, x @ W1 + b1)
|
|
logits = h @ W2 + b2
|
|
return int(np.argmax(logits))
|
|
|
|
|
|
class Agent:
|
|
def __init__(self, net: NeuralNet, start_row: int, start_col: int):
|
|
self.net = net
|
|
self.row = start_row
|
|
self.col = start_col
|
|
self.alive = True
|
|
self.reached_goal = False
|
|
self.steps = 0
|
|
self.fitness = 0.0
|
|
self.trail: list[tuple[int,int]] = [] # pixel coords
|
|
self.dist_traveled_toward = 0.0
|
|
|
|
def _pixel_center(self, cfg) -> tuple[int, int]:
|
|
return (self.col * cfg.cell_size + cfg.cell_size // 2,
|
|
self.row * cfg.cell_size + cfg.cell_size // 2)
|
|
|
|
def get_inputs(self, grid, goal_row, goal_col, cfg) -> np.ndarray:
|
|
wN, wS, wE, wW = get_walls(grid, self.row, self.col)
|
|
dx = (goal_col - self.col) / cfg.maze_cols
|
|
dy = (goal_row - self.row) / cfg.maze_rows
|
|
px = self.col / cfg.maze_cols
|
|
py = self.row / cfg.maze_rows
|
|
return np.array([wN, wS, wE, wW, dx, dy, px, py], dtype=np.float32)
|
|
|
|
def step(self, grid, goal_row, goal_col, cfg):
|
|
if self.reached_goal:
|
|
return
|
|
x = self.get_inputs(grid, goal_row, goal_col, cfg)
|
|
action = self.net.forward(x)
|
|
dr = [-1, 1, 0, 0][action]
|
|
dc = [0, 0, 1, -1][action]
|
|
prev_dist = abs(goal_row - self.row) + abs(goal_col - self.col)
|
|
if is_passable(grid, self.row, self.col, action):
|
|
self.row += dr
|
|
self.col += dc
|
|
new_dist = abs(goal_row - self.row) + abs(goal_col - self.col)
|
|
self.dist_traveled_toward += max(0.0, prev_dist - new_dist)
|
|
self.steps += 1
|
|
self.trail.append(self._pixel_center(cfg))
|
|
if self.row == goal_row and self.col == goal_col:
|
|
self.reached_goal = True
|
|
|
|
def compute_fitness(self, goal_row, goal_col, cfg):
|
|
dist = abs(goal_row - self.row) + abs(goal_col - self.col)
|
|
self.fitness = (
|
|
self.dist_traveled_toward
|
|
- dist * 0.5
|
|
+ (cfg.goal_bonus if self.reached_goal else 0.0)
|
|
- self.steps * cfg.step_penalty
|
|
)
|
|
return self.fitness
|