Micromouse/src/algorithm.zig

275 lines
9.0 KiB
Zig

const std = @import("std");
const assert = std.debug.assert;
const map = @import("map.zig");
const Point = map.Point;
const Cardinal = map.Cardinal;
const Mouse = @import("mouse.zig").Mouse;
pub const is_robot = @import("builtin").os.tag == .freestanding;
// Doesn't have any state
// It's a hardware abstraction layer
pub const robot = if (is_robot) @import("rp2040-bot.zig") else @import("simulated-bot.zig");
// It's a little weird to do this as a global, but it just returns
// an instance of Mouse with initial values
var mouse = @import("mouse.zig").initialize();
pub fn setup () void {
robot.setup();
}
//// Node
const Node = struct {
p: Point,
score: usize,
// Return 0-4 Nodes, by checking if
// each of the four nodes around this node a) exists and b) is accessible (no wall)
// I maybe could've also returned a `[4:0]*Node` (sentinel terminated 4-long array of node pointers)
pub fn getAdjacentNodes(self: Node) std.BoundedArray(*Node, 4) {
var nodes = std.BoundedArray(*Node, 4).init(0) catch unreachable; // 0 is < 4
// I could clean this up with like a loop and a helper function
// North
// std.log.debug("North: y + 1 < height {}; not wall: {}", .{self.p.y + 1 < map.height, !isWall(self.p, .north)});
// std.log.debug("isWall(self.p '{}', .north): {}", .{self.p, isWall(self.p, .north)});
if (self.p.y + 1 < map.height and !isWall(self.p, .north)) {
// std.log.debug("Adding node", .{});
nodes.append(&mazeNodes[self.p.y + 1][self.p.x]) catch unreachable;
}
// South
if (self.p.y > 0 and !isWall(self.p, .south)) {
nodes.append(&mazeNodes[self.p.y - 1][self.p.x]) catch unreachable;
}
// East
if (self.p.x + 1 < map.width and !isWall(self.p, .east)) {
nodes.append(&mazeNodes[self.p.y][self.p.x + 1]) catch unreachable;
}
// West
if (self.p.x > 0 and !isWall(self.p, .west)) {
nodes.append(&mazeNodes[self.p.y][self.p.x - 1]) catch unreachable;
}
return nodes;
}
pub fn updateScore(self: *Node, newScore: usize) void {
// newScore should be less than the current score
assert(newScore <= self.score);
self.score = newScore;
// Call updateScore on all adjacent nodes if they have a score which is worse than (newScore + 1)
// For some reason BoundedArrays aren't indexable
const adjacentNodes = self.getAdjacentNodes().slice();
for (adjacentNodes) |node| {
if (self.score + 1 < node.score) {
node.updateScore(self.score + 1);
}
}
if (!is_robot) {
robot.writeCellScore(self.p, self.score);
}
}
};
//// Walls
// I tried to come up with a better data structure for this, and it just hurt my brain too much
// So I'm just going to go with the Matthias classic
// With a slight change for our coordinate system
// 11 x 11 list of "wall posts" with each wall post keeping track of the wall to its north and east
const WallPost = struct {
// Not using Cardinal or Point here to avoid confusing with the maze tile directions
// These are nullable to represent when we haven't seen them yet
up: ?bool,
right: ?bool,
};
// Might be a better way to do this initialization
var walls = init: {
var initialWalls: [map.height + 1][map.width + 1]WallPost = undefined;
var row: [map.width + 1]WallPost = undefined;
@memset(&row, WallPost { .up = null, .right = null });
@memset(&initialWalls, row);
break :init initialWalls;
};
fn isEdge(p: Point, direction: Cardinal) bool {
// std.log.debug("is Edge called with point: {} direction: {}\n", .{p, direction});
return (
(p.x == 0 and direction == .west)
or (p.y == 0 and direction == .south)
or (p.x == map.width - 1 and direction == .east)
or (p.y == map.height - 1 and direction == .north)
);
}
// Used by both isWall and setWall
fn getWall(p: Point, direction: Cardinal) *?bool {
// Should be handled by the caller
assert(!isEdge(p, direction));
// Direction is north, west, or south we look left.
const wallX = if (direction != .east) p.x else p.x + 1;
// We look down, unless it's north
const wallY = if (direction != .north) p.y else p.y + 1;
const wallPost = &walls[wallY][wallX];
return &(if (direction == .west or direction == .east) wallPost.up else wallPost.right);
}
fn isWall(p: Point, direction: Cardinal) bool {
// Boundaries are definitely there
if (isEdge(p, direction)) {
// std.log.debug("forcing true return from isWall for point {} and direction {}", .{p, direction});
return true;
}
const wall = getWall(p, direction);
// Now, because floodfill is optimistic, I'm going to default to assuming there's not a wall if we haven't seen one
return wall.* orelse false;
}
pub fn setWall(p: Point, direction: Cardinal, value: bool) void {
// We should maybe have a better way of handling this if it actually happens to the robot
// If that direction is an outer wall, assert that we saw a wall
// TODO: cleanup
if (isEdge(p, direction)) {
// std.log.debug("Hello? {} {} {} {}", .{p, direction, isEdge(p, direction), value});
assert(value);
// print("Hello? {} {} {} {}", .{p, direction, isEdge(p, direction), value});
// std.debug.panic("Outside of maze is ");
}
// assert(!(isEdge(p, direction) and !value));
if (!is_robot and value) {
robot.showWall(p, direction);
}
if (isEdge(p, direction)) {
return;
}
const wall = getWall(p, direction);
// wall.* is a ?bool and value is a bool, but we can do type coercion
wall.* = value;
}
//// Maze
var mazeNodes = init: {
var nodes: [map.width][map.height]Node = undefined;
for (&nodes, 0..) |*row, y| {
for (row, 0..) |*n, x| {
n.* = Node {
.p = Point{ .x = x, .y = y },
.score = std.math.maxInt(@TypeOf(n.score))
};
}
}
break :init nodes;
};
test {
// y, x; and we start in the bottom left
const s = mazeNodes[0][0];
// @compileLog(s.p.x);
// @compileLog(s.p.y);
// @compileLog(map.start.x);
// @compileLog(map.start.y);
assert(s.p.x == map.start.x);
assert(s.p.y == map.start.y);
}
//// Algorithm
fn recalcScores(dest: []const Point) void {
// Reset every node to a high score
for (&mazeNodes) |*row| {
for (row) |*n| {
n.score = std.math.maxInt(@TypeOf(n.score));
}
}
// Create destination nodes for every destination point, with a score of 0
// Starting at each of the destination(s) and working out, give each node an increasing score
// updateScore handles updating the scores for all other nodes in the maze
for (dest) |p| {
const goalNode = &mazeNodes[p.y][p.x];
goalNode.updateScore(0);
}
}
// Just a helper function
fn hasPoint(l: []const Point, needle: Point) bool {
for (l) |p| {
if (needle.eq(p)) {
return true;
}
}
return false;
}
// Takes a list of destination points, and a mouse
// Blocks until the mouse is in one of the destination points
pub fn floodFill(dest: []const Point) void {
if (dest.len == 4) {
assert(hasPoint(dest, map.goals[0]));
assert(hasPoint(dest, map.goals[3]));
}
mouse.updateWalls();
// Calculate optimistic distances from the center
recalcScores(dest);
for (dest) |p| {
const goalNode = &mazeNodes[p.y][p.x];
std.log.debug("goal nodes: {}\n", .{ goalNode });
}
// While we aren't at dest,
while (!hasPoint(dest[0..], mouse.position)) {
const currentSquare = &mazeNodes[mouse.position.y][mouse.position.x];
// 1. get the square adjacent to the mouse which has the lowest score
var minAdjacent: ?*Node = null;
std.log.debug("currentSquare {} score: {}", .{ currentSquare.p, currentSquare.score });
const adjacentNodes = currentSquare.getAdjacentNodes().slice();
std.log.debug("adjacentNodes {}", .{adjacentNodes.len});
for (adjacentNodes) |n| {
std.log.debug("Node at ({}, {}) has score {}", .{ n.p.x, n.p.y, n.score });
assert(@TypeOf(n) == *Node);
if (minAdjacent == null or n.score < minAdjacent.?.score) {
minAdjacent = n;
}
}
assert(minAdjacent != null);
// 2. if that square has a value the same or higher than the current score, then we re-do score calculation and go to 1
if (minAdjacent.?.score >= currentSquare.score) {
recalcScores(dest);
continue;
}
// 3. otherwise, move the mouse there
mouse.moveAdjacent(minAdjacent.?.p);
// 4. update the sensors on the mouse
mouse.updateWalls();
}
assert(hasPoint(dest, mouse.position));
if (dest.len == 4) {
// This isn't always true because sometimes dest isn't the goal
assert(hasPoint(map.goals[0..], mouse.position));
}
}