Skip to content

Commit

Permalink
Merge pull request #146 from d4rk/snake
Browse files Browse the repository at this point in the history
New Game – Snake
  • Loading branch information
ThePrimeagen authored Dec 5, 2024
2 parents 4fa57b7 + afd8b21 commit 0ae3de1
Show file tree
Hide file tree
Showing 7 changed files with 562 additions and 2 deletions.
12 changes: 10 additions & 2 deletions lua/vim-be-good/game-runner.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ local WordRound = require("vim-be-good.games.words");
local CiRound = require("vim-be-good.games.ci");
local HjklRound = require("vim-be-good.games.hjkl");
local WhackAMoleRound = require("vim-be-good.games.whackamole");
local Snake = require("vim-be-good.games.snake");
local log = require("vim-be-good.log");
local statistics = require("vim-be-good.statistics");

Expand Down Expand Up @@ -42,6 +43,10 @@ local games = {
whackamole = function(difficulty, window)
return WhackAMoleRound:new(difficulty, window)
end,

snake = function(difficulty, window)
return Snake:new(difficulty, window)
end,
}

local runningId = 0
Expand Down Expand Up @@ -300,6 +305,9 @@ function GameRunner:run()
self.window.buffer:debugLine(string.format(
"Round %d / %d", self.currentRound, self.config.roundCount))

if roundConfig.canEndRound then
self.round:setEndRoundCallback(function() self:endRound() end)
end
self.window.buffer:setInstructions(self.round.getInstructions())
local lines, cursorLine, cursorCol = self.round:render()
self.window.buffer:render(lines)
Expand All @@ -312,8 +320,8 @@ function GameRunner:run()
cursorLine = cursorLine + curRoundLineLen + instuctionLen

log.info("Setting current line to", cursorLine, cursorCol)
if cursorLine > 0 then
vim.api.nvim_win_set_cursor(0, {cursorLine, cursorCol})
if cursorLine > 0 and not roundConfig.noCursor then
vim.api.nvim_win_set_cursor(0, {cursorLine, cursorCol})
end

self.startTime = GameUtils.getTime()
Expand Down
75 changes: 75 additions & 0 deletions lua/vim-be-good/games/snake.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
local log = require("vim-be-good.log")
local GameUtils = require("vim-be-good.game-utils")
local SnakeGame = require("vim-be-good.games.snakelib.snakegame")
local T = require("vim-be-good.types")

local Snake = {}

function Snake:new(difficulty, window)
local getDifficultyLevel = function(d)
for i, val in ipairs(T.difficulty) do
if d == val then
return i
end
end
return 0
end
local round = {
window = window,
difficulty = difficulty,
difficultyLevel = getDifficultyLevel(difficulty),
endRoundCallback = nil
}
self.__index = self
return setmetatable(round, self)
end

function Snake:getInstructions()
return {
'',
'Classic game of Snake.',
'',
'1. h,j,k,l to navigate',
' h - move left',
' j - move down',
' k - move up ',
' l - move right',
'2. Eat food (O) to grow',
'3. Don\'t eat yourself',
'4. In higher difficulties, walls kill',
'5. Snake speed scales with difficulty',
}
end

function Snake:getConfig()
log.info("getConfig", self.difficulty, GameUtils.difficultyToTime[self.difficulty])
return {
roundTime = 1000000,
noCursor = true,
canEndRound = true,
}
end

function Snake:checkForWin()
log.info('Checking for Win')
return false
end

function Snake:name()
return 'snake'
end

function Snake:setEndRoundCallback(endRoundCallback)
self.endRoundCallback = endRoundCallback
end

function Snake:render()
if self.snakeGame then
self.snakeGame:shutdown(nil)
end
self.snakeGame = SnakeGame:new(35, 15, self.difficultyLevel, self.endRoundCallback)
self.snakeGame:start()
return {''}, 1
end

return Snake
37 changes: 37 additions & 0 deletions lua/vim-be-good/games/snakelib/const.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
local STOPPED=0
local UP = 1
local DOWN=2
local LEFT=3
local RIGHT=4

local Directions = {
STOPPED = 0,
UP = 1,
DOWN = 2,
LEFT = 3,
RIGHT = 4,
}

local HeadChar = {
[UP] = '^',
[DOWN] = 'v',
[LEFT] = '<',
[RIGHT] = '>',
[STOPPED] = 'X',
}

local KeyDirMap = {
h = LEFT,
j = DOWN,
k = UP,
l = RIGHT
}

return {
Directions = Directions,
HeadChar = HeadChar,
KeyDirMap = KeyDirMap,
BodyChar = '*',
FoodChar = 'O',
GridChar = '.',
}
89 changes: 89 additions & 0 deletions lua/vim-be-good/games/snakelib/grid.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
local V = vim.api
local C = require('vim-be-good.games.snakelib.const')
local log = require("vim-be-good.log")

local Grid = {}

-- Class the represents a textual rendering grid.
-- @param bufNum integer: the underlying vim buffer to use
-- @param cols integer: the number of columns (width)
-- @param rows integer: the number of rows (height)
-- @param fillChar string: the default character to use
-- when clearing the grid
function Grid:new(bufNum, cols, rows, fillChar)
self.__index = self
if not fillChar then
fillChar = C.GridChar
end
local lines = {}
for _ = 1, rows do
table.insert(lines, string.rep(fillChar, cols))
end
local newGrid = {
buf = bufNum,
lines = lines,
rows = rows,
cols = cols,
width = cols,
height = rows,
fillChar = fillChar
}
return setmetatable(newGrid, self)
end

-- Clears the grid using the given @param fillChar, or the
-- default fillChar if none is provided
function Grid:clear(fillChar)
if not fillChar then
fillChar = self.fillChar
end
local lines = {}
for _ = 1, self.rows do
table.insert(lines, string.rep(fillChar, self.cols))
end
self.lines = lines
end

-- Sets the character at the given position to @param char
function Grid:setChar(col, row, char)
row = row + 1
col = col + 1
if row < 1 or col < 1 or row > self.rows or col > self.cols then
log.warn("setChar ("..char..") out of bounds: " .. col .. "," .. row)
return
end
local line = self.lines[row]
line = string.sub(line, 1, col-1) .. char .. string.sub(line, col+1)
self.lines[row] = line
end

-- Returns the character at the given position.
function Grid:getChar(col, row)
row = row + 1
col = col + 1
if row < 1 or col < 1 or row > self.rows or col > self.cols then
log.warn("getChar out of bounds: " .. col .. "," .. row)
return nil
end
local line = self.lines[row]
return string.sub(line, col, col)
end

-- Draws the grid to the text buffer associated with the grid.
-- The buffer is marked non-modifiable at the end of the render.
function Grid:render()
local currentLines = V.nvim_buf_get_lines(self.buf, 0, -1, false)
local rows = #currentLines
local maxCols = 0
for _, row in pairs(currentLines) do
local lineLen = string.len(row)
if lineLen > maxCols then
maxCols = lineLen
end
end
V.nvim_buf_set_option(self.buf, 'modifiable', true)
V.nvim_buf_set_text(self.buf, 0, 0, rows-1, maxCols, self.lines)
V.nvim_buf_set_option(self.buf, 'modifiable', false)
end

return Grid
139 changes: 139 additions & 0 deletions lua/vim-be-good/games/snakelib/snake.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
local Snake = {}

local C = require('vim-be-good.games.snakelib.const')
local Dir = C.Directions

local STOPPED = Dir.STOPPED
local UP = Dir.UP
local DOWN = Dir.DOWN
local LEFT = Dir.LEFT
local RIGHT = Dir.RIGHT

-- Represents the snake object (consists of a head and a body).
-- @param x integer: the x coordinate of the head
-- @param y integer: the y coordinate of the head
-- @param initialSize integer: the initial size of the body
-- @param noWalls boolean: if true, then the snake loops
-- around when it hits a wall
function Snake:new(x, y, initialSize, noWalls)
self.__index = self
local head = { x = math.floor(x), y = math.floor(y) }
local newSnake = {
dir = RIGHT,
head = {x = math.floor(x), y = math.floor(y)},
body = {},
shouldGrow = false,
dead = false,
noWalls = noWalls,
hitWall = false,
}
if initialSize and initialSize > 1 then
local lastBodyPart = head
for _ = 1, initialSize do
local bodyPart = { x = lastBodyPart.x - 1, y = lastBodyPart.y }
lastBodyPart = bodyPart
table.insert(newSnake.body, bodyPart)
end
end
return setmetatable(newSnake, self)
end

-- Sets the direction of the snake.
-- Snakes can't reverse their direction, so not all
-- combinations are valid.
-- @param dir Direction: one of the direction enums
function Snake:setDir(dir)
if self.dead or dir < UP or dir > RIGHT then
return
end
local curDir = self.dir
-- Snakes can't reverse
if curDir == LEFT and dir == RIGHT or
curDir == RIGHT and dir == LEFT or
curDir == UP and dir == DOWN or
curDir == DOWN and dir == UP then
return
end
self.dir = dir
end

-- Notifies that snake that it should grow its body
-- length by 1. Called when it eats food.
function Snake:grow()
self.shouldGrow = true
end

-- Moves the snake in its current direction, and updates its
-- internal state. Takes into account walls, growth, etc.
-- @param grid Grid: the grid object that the snake can move it
function Snake:tick(grid)
local head = self.head
local dir = self.dir
if dir == STOPPED then
return
end
-- Move tail to current head
if #self.body > 0 then
local tail = {}
if not self.shouldGrow then
tail = table.remove(self.body)
end
self.shouldGrow = false
tail.x = head.x
tail.y = head.y
table.insert(self.body, 1, tail)
end
-- Move head in direction
if dir == UP then
head.y = head.y - 1
elseif dir == DOWN then
head.y = head.y + 1
elseif dir == LEFT then
head.x = head.x - 1
elseif dir == RIGHT then
head.x = head.x + 1
end
-- Loop around if at edge of screen
if head.x >= grid.width then
self:handleWallHit(function () head.x = 0 end)
elseif head.x < 0 then
self:handleWallHit(function () head.x = grid.width end)
elseif head.y >= grid.height then
self:handleWallHit(function () head.y = 0 end)
elseif head.y < 0 then
self:handleWallHit(function () head.y = grid.height end)
else
self.hitWall = false
end
end

-- Handles collision with walls
function Snake:handleWallHit(noWallsCallback)
if self.noWalls then
-- If no walls, execute the callback, which loops the head
-- to the opposite edge
noWallsCallback()
end
self.hitWall = true
end

-- Draws the snake on the provided grid.
function Snake:renderBody(grid)
for _, body in pairs(self.body) do
grid:setChar(body.x, body.y, C.BodyChar)
end
end

-- Draws the snake's head on the provide grid.
function Snake:renderHead(grid)
local head = self.head
grid:setChar(head.x, head.y, C.HeadChar[self.dir])
end

-- Called when the snake has met an untimely demise.
function Snake:oops()
self.dead = true
self.dir = STOPPED
end

return Snake
Loading

0 comments on commit 0ae3de1

Please sign in to comment.