Compare commits
15 Commits
9edf9bfb24
...
prod
| Author | SHA1 | Date | |
|---|---|---|---|
|
a5737a1322
|
|||
|
ef70fb1ce4
|
|||
|
13b39a73b4
|
|||
|
3d1ed0ef29
|
|||
|
83bca4a02a
|
|||
|
b0cfffc021
|
|||
|
d6a085414c
|
|||
|
ead4757116
|
|||
|
4b583bf737
|
|||
|
15d6ade753
|
|||
|
78625ee302
|
|||
|
7668933510
|
|||
|
7d087a80cb
|
|||
|
d6175bb84a
|
|||
|
b45a77f76d
|
17
README.md
Normal file
17
README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Snake
|
||||
|
||||
This is a small little experiment with Zig and Raylib to implement snake.
|
||||
|
||||
Prerequsites:
|
||||
* [Zig](https://ziglang.org/learn/getting-started/)
|
||||
|
||||
Run command:
|
||||
```zig
|
||||
zig build run
|
||||
```
|
||||
|
||||
Test Command
|
||||
```zig
|
||||
zig build test
|
||||
```
|
||||
|
||||
11
build.zig
11
build.zig
@@ -43,12 +43,13 @@ pub fn build(b: *std.Build) void {
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
const exe_unit_tests = b.addTest(.{
|
||||
.root_module = exe_mod,
|
||||
});
|
||||
const main_tests = b.addTest(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize });
|
||||
const game_tests = b.addTest(.{ .root_source_file = b.path("src/game.zig"), .target = target, .optimize = optimize });
|
||||
|
||||
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
|
||||
const run_exe_main_tests = b.addRunArtifact(main_tests);
|
||||
const run_exe_game_tests = b.addRunArtifact(game_tests);
|
||||
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
test_step.dependOn(&run_exe_unit_tests.step);
|
||||
test_step.dependOn(&run_exe_main_tests.step);
|
||||
test_step.dependOn(&run_exe_game_tests.step);
|
||||
}
|
||||
|
||||
268
src/game.zig
268
src/game.zig
@@ -1,78 +1,236 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Direction = enum {
|
||||
pub const Direction = enum {
|
||||
up,
|
||||
down,
|
||||
left,
|
||||
right,
|
||||
|
||||
fn vector(self: Direction) Vec2 {
|
||||
return switch (self) {
|
||||
Direction.up => Vec2{ 0, -1 },
|
||||
Direction.down => Vec2{ 0, 1 },
|
||||
Direction.left => Vec2{ -1, 0 },
|
||||
Direction.right => Vec2{ 1, 0 },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// x, y
|
||||
const Vec2 = @Vector(2, u32);
|
||||
const Vec2 = @Vector(2, i32);
|
||||
|
||||
const Element = struct {
|
||||
position: Vec2,
|
||||
|
||||
prev: ?Element,
|
||||
next: ?Element,
|
||||
|
||||
pub fn hasPrev(self: Element) bool {
|
||||
return self.prev != null;
|
||||
}
|
||||
pub fn hasNext(self: Element) bool {
|
||||
return self.next != null;
|
||||
}
|
||||
pub const State = enum {
|
||||
running,
|
||||
lost,
|
||||
};
|
||||
|
||||
const GameState = struct {
|
||||
pub const Game = struct {
|
||||
playFieldSize: Vec2,
|
||||
direction: Direction,
|
||||
body: std.ArrayList(Vec2),
|
||||
state: State,
|
||||
rng: std.Random.DefaultPrng,
|
||||
|
||||
food: Vec2,
|
||||
grow: bool,
|
||||
|
||||
headDirection: Direction,
|
||||
|
||||
head: Element,
|
||||
tail: Element,
|
||||
|
||||
pub fn create() GameState {
|
||||
const pos1 = Element{
|
||||
.position = Vec2{ 10, 10 },
|
||||
.prev = null,
|
||||
.next = null,
|
||||
};
|
||||
const pos2 = Element{
|
||||
.position = Vec2{ 9, 10 },
|
||||
.prev = pos1,
|
||||
.next = null,
|
||||
};
|
||||
const pos3 = Element{
|
||||
.position = Vec2{ 8, 10 },
|
||||
.prev = pos2,
|
||||
.next = null,
|
||||
};
|
||||
|
||||
pos1.next = pos2;
|
||||
pos2.next = pos3;
|
||||
|
||||
return GameState{
|
||||
.playFieldSize = Vec2{ 20, 20 },
|
||||
.food = Vec2{ 2, 5 },
|
||||
.headDirection = Direction.left,
|
||||
.head = pos1,
|
||||
.tail = pos3,
|
||||
};
|
||||
pub fn score(self: Game) usize {
|
||||
return self.body.items.len;
|
||||
}
|
||||
|
||||
pub fn tick() void {}
|
||||
pub fn createWithSize(allocator: std.mem.Allocator, bodySize: u8) !Game {
|
||||
var body = try std.ArrayList(Vec2).initCapacity(allocator, 1000);
|
||||
|
||||
fn calculateSnakePositions(self: GameState, allocator: std.Allocator) []Vec2 {
|
||||
var array = std.ArrayList(u8).initCapacity(allocator, 1000);
|
||||
|
||||
var current = self.head;
|
||||
while (current.hasNext()) {
|
||||
array.add(current.position);
|
||||
const startPos: u8 = 10;
|
||||
for (0..bodySize) |i| {
|
||||
const index = std.math.cast(i32, i) orelse unreachable;
|
||||
try body.append(Vec2{ startPos - index, 10 });
|
||||
}
|
||||
|
||||
return array;
|
||||
const seed: u64 = @intCast(std.time.nanoTimestamp());
|
||||
const prng = std.Random.DefaultPrng.init(seed);
|
||||
|
||||
return Game{
|
||||
.playFieldSize = Vec2{ 20, 20 },
|
||||
.food = Vec2{ 11, 13 },
|
||||
.direction = Direction.right,
|
||||
.body = body,
|
||||
.state = State.running,
|
||||
.rng = prng,
|
||||
.grow = false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn create(allocator: std.mem.Allocator) !Game {
|
||||
return createWithSize(allocator, 3);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Game) void {
|
||||
self.body.deinit();
|
||||
}
|
||||
|
||||
pub fn setDirection(self: *Game, newDirection: Direction) void {
|
||||
if (self.direction == newDirection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const headPos = self.body.items[0];
|
||||
const firstBodyPart = self.body.items[1];
|
||||
|
||||
if (std.meta.eql(headPos + newDirection.vector(), firstBodyPart)) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.direction = newDirection;
|
||||
}
|
||||
|
||||
pub fn tick(self: *Game) !void {
|
||||
if (self.state != State.running) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newHead = self.body.items[0] + self.direction.vector();
|
||||
if (newHead[0] == 0 or newHead[0] == self.playFieldSize[0] or
|
||||
newHead[1] == 0 or newHead[1] == self.playFieldSize[1])
|
||||
{
|
||||
self.state = State.lost;
|
||||
return;
|
||||
}
|
||||
|
||||
for (self.body.items) |pos| {
|
||||
if (std.meta.eql(pos, newHead)) {
|
||||
self.state = State.lost;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try self.body.insert(0, newHead);
|
||||
if (self.grow) {
|
||||
self.grow = false;
|
||||
} else {
|
||||
_ = self.body.pop();
|
||||
}
|
||||
|
||||
if (std.meta.eql(newHead, self.food)) {
|
||||
self.food = self.getEmptySpace();
|
||||
self.grow = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn getEmptySpace(self: *Game) Vec2 {
|
||||
outer: while (true) {
|
||||
const x = self.rng.random().intRangeLessThan(i32, 1, self.playFieldSize[0]);
|
||||
const y = self.rng.random().intRangeLessThan(i32, 1, self.playFieldSize[1]);
|
||||
|
||||
const potentialResult = Vec2{ x, y };
|
||||
for (self.body.items) |pos| {
|
||||
if (std.meta.eql(pos, potentialResult)) {
|
||||
continue :outer;
|
||||
}
|
||||
}
|
||||
|
||||
return potentialResult;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
test "should initialize game" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
var game = try Game.create(allocator);
|
||||
defer game.deinit();
|
||||
const actual = game.body.items;
|
||||
const expected = [_]Vec2{ Vec2{ 10, 10 }, Vec2{ 9, 10 }, Vec2{ 8, 10 } };
|
||||
|
||||
try testing.expectEqualSlices(Vec2, &expected, actual);
|
||||
try testing.expectEqual(Direction.right, game.direction);
|
||||
try testing.expectEqual(Vec2{ 20, 20 }, game.playFieldSize);
|
||||
try testing.expectEqual(State.running, game.state);
|
||||
}
|
||||
|
||||
test "should move down" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
var game = try Game.create(allocator);
|
||||
defer game.deinit();
|
||||
|
||||
game.setDirection(Direction.down);
|
||||
try testing.expectEqual(Direction.down, game.direction);
|
||||
|
||||
try game.tick();
|
||||
const expected = [_]Vec2{ Vec2{ 10, 11 }, Vec2{ 10, 10 }, Vec2{ 9, 10 } };
|
||||
try testing.expectEqualSlices(Vec2, &expected, game.body.items);
|
||||
}
|
||||
|
||||
test "should not be able to change direction to the previous position" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
var game = try Game.create(allocator);
|
||||
defer game.deinit();
|
||||
|
||||
game.setDirection(Direction.left);
|
||||
try testing.expectEqual(Direction.right, game.direction);
|
||||
}
|
||||
|
||||
test "should loose if exiting playField right" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
var game = try Game.create(allocator);
|
||||
defer game.deinit();
|
||||
|
||||
for (0..10) |_| {
|
||||
try game.tick();
|
||||
}
|
||||
|
||||
const expected = [_]Vec2{ Vec2{ 19, 10 }, Vec2{ 18, 10 }, Vec2{ 17, 10 } };
|
||||
try testing.expectEqualSlices(Vec2, &expected, game.body.items);
|
||||
|
||||
try testing.expectEqual(State.lost, game.state);
|
||||
}
|
||||
|
||||
test "should loose if touching tail" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
var game = try Game.createWithSize(allocator, 4);
|
||||
defer game.deinit();
|
||||
|
||||
game.setDirection(Direction.down);
|
||||
try game.tick();
|
||||
game.setDirection(Direction.left);
|
||||
try game.tick();
|
||||
try testing.expectEqual(State.running, game.state);
|
||||
game.setDirection(Direction.up);
|
||||
try game.tick();
|
||||
try testing.expectEqual(State.lost, game.state);
|
||||
|
||||
const expected = [_]Vec2{ Vec2{ 9, 11 }, Vec2{ 10, 11 }, Vec2{ 10, 10 }, Vec2{ 9, 10 } };
|
||||
try testing.expectEqualSlices(Vec2, &expected, game.body.items);
|
||||
|
||||
try testing.expectEqual(State.lost, game.state);
|
||||
}
|
||||
|
||||
test "should grow and spawn new food on ate food" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
var game = try Game.create(allocator);
|
||||
defer game.deinit();
|
||||
|
||||
try testing.expect(std.meta.eql(game.food, Vec2{ 11, 13 }));
|
||||
|
||||
try game.tick();
|
||||
game.setDirection(Direction.down);
|
||||
try game.tick();
|
||||
try game.tick();
|
||||
try game.tick();
|
||||
|
||||
const expected = [_]Vec2{ Vec2{ 11, 13 }, Vec2{ 11, 12 }, Vec2{ 11, 11 } };
|
||||
try testing.expectEqualSlices(Vec2, &expected, game.body.items);
|
||||
try testing.expect(!std.meta.eql(game.food, Vec2{ 11, 13 }));
|
||||
|
||||
try game.tick();
|
||||
|
||||
const expected2 = [_]Vec2{ Vec2{ 11, 14 }, Vec2{ 11, 13 }, Vec2{ 11, 12 }, Vec2{ 11, 11 } };
|
||||
try testing.expectEqualSlices(Vec2, &expected2, game.body.items);
|
||||
}
|
||||
|
||||
51
src/main.zig
51
src/main.zig
@@ -1,16 +1,19 @@
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const g = @import("game.zig");
|
||||
|
||||
pub fn main() anyerror!void {
|
||||
const alloc = std.heap.page_allocator;
|
||||
const allocator = std.heap.page_allocator;
|
||||
const scaleFactor = 20;
|
||||
|
||||
const screenWidth = 800;
|
||||
const screenHeight = 450;
|
||||
const screenWidth = 1000;
|
||||
const screenHeight = 600;
|
||||
|
||||
rl.initWindow(screenWidth, screenHeight, "Snake");
|
||||
defer rl.closeWindow();
|
||||
rl.setTargetFPS(2);
|
||||
|
||||
rl.setTargetFPS(100);
|
||||
var game = try g.Game.create(allocator);
|
||||
|
||||
while (!rl.windowShouldClose()) {
|
||||
rl.beginDrawing();
|
||||
@@ -18,14 +21,38 @@ pub fn main() anyerror!void {
|
||||
|
||||
rl.clearBackground(.white);
|
||||
|
||||
const info = try std.fmt.allocPrintZ(alloc, "FPS: {}\n", .{rl.getFPS()});
|
||||
if (rl.isKeyPressed(.up)) {
|
||||
game.setDirection(g.Direction.up);
|
||||
}
|
||||
if (rl.isKeyPressed(.down)) {
|
||||
game.setDirection(g.Direction.down);
|
||||
}
|
||||
if (rl.isKeyPressed(.right)) {
|
||||
game.setDirection(g.Direction.right);
|
||||
}
|
||||
if (rl.isKeyPressed(.left)) {
|
||||
game.setDirection(g.Direction.left);
|
||||
}
|
||||
|
||||
rl.drawText("Congrats! You created your first window!", 190, 200, 20, .light_gray);
|
||||
rl.drawText(info, 0, 0, 20, .light_gray);
|
||||
rl.drawRectangle(10, 10, 10, 10, .black);
|
||||
try game.tick();
|
||||
|
||||
// Border
|
||||
rl.drawRectangle(0, 0, scaleFactor, game.playFieldSize[1] * scaleFactor, .black);
|
||||
rl.drawRectangle(0, 0, game.playFieldSize[0] * scaleFactor, scaleFactor, .black);
|
||||
rl.drawRectangle(game.playFieldSize[0] * scaleFactor, 0, scaleFactor, game.playFieldSize[1] * scaleFactor, .black);
|
||||
rl.drawRectangle(0, game.playFieldSize[1] * scaleFactor, game.playFieldSize[0] * scaleFactor, scaleFactor, .black);
|
||||
rl.drawRectangle(game.playFieldSize[0] * scaleFactor, game.playFieldSize[1] * scaleFactor, scaleFactor, scaleFactor, .black);
|
||||
|
||||
// Food
|
||||
rl.drawRectangle(game.food[0] * scaleFactor, game.food[1] * scaleFactor, scaleFactor, scaleFactor, .red);
|
||||
|
||||
// Snake
|
||||
for (game.body.items) |part| {
|
||||
rl.drawRectangle(part[0] * scaleFactor, part[1] * scaleFactor, scaleFactor, scaleFactor, .green);
|
||||
}
|
||||
|
||||
if (game.state == g.State.lost) {
|
||||
rl.drawText("Congrats! You lost!", 190, 200, 20, .light_gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "should initialize game" {
|
||||
try std.testing.expect(42 == 42);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user