Skip to content

Instantly share code, notes, and snippets.

@nico-abram
Created September 8, 2018 23:39
Show Gist options
  • Save nico-abram/c3445d1a78b755031a020fbce3eeca12 to your computer and use it in GitHub Desktop.
Save nico-abram/c3445d1a78b755031a020fbce3eeca12 to your computer and use it in GitHub Desktop.
--[[
GridActor handles the update and input callback registering.
It also contains all the grid actors (Quads, or "Coloured rectangles")
The config table has all the basic configurable game parameters
The grid table contains a matrix of cells, used by gridActor quads to draw themselves
A cell can either be nil (Empty) or have a block
A block is a lua table which has a color
the currentPiece variable is a table with a 1 character string (type), a
a rotation (Number 0-3) and an offset (How many cells it has fallen) number
inputCallback is the inputCallback function
]]
--config
local config = {
grid = {
height = 20,
width = 10,
blockWidth = 20,
blockHeight = 20,
},
pieces = {
colors = {
I = color("#8888ffCC"),
J = color("#bbbbbbCC"),
L = color("#0000ffCC"),
T = color("#FF3333CC"),
Z = color("#BB7777CC"),
S = color("#00ff00CC"),
O = color("#FFAAAACC"),
}
},
buttons = {
speedUp = "u",
rotateLeft = "z",
rotateRight = "x",
rotate180 = ",",
drop = " ",
down = "down",
up = "up",
left = "left",
right = "right",
},
bgColor = color("#333333CC"),
normalSpeed = 2,
highSpeed = 0.02,
inputPollingSeconds = 0.1,
}
-- General utility functions
function tableKeys(t)
local keys={}
local n=0
for k,v in pairs(t) do
n=n+1
keys[n]=k
end
return keys
end
function shuffle(tbl)
size = #tbl
for i = size, 1, -1 do
local rand = math.random(size)
tbl[i], tbl[rand] = tbl[rand], tbl[i]
end
return tbl
end
function serializeTable(val, name, skipnewlines, depth)
skipnewlines = skipnewlines or false
depth = depth or 0
local tmp = string.rep(" ", depth)
if name then tmp = tmp .. name .. " = " end
if type(val) == "table" then
tmp = tmp .. "{" .. (not skipnewlines and "\n" or "")
for k, v in pairs(val) do
tmp = tmp .. serializeTable(v, k, skipnewlines, depth + 1) .. "," .. (not skipnewlines and "\n" or "")
end
tmp = tmp .. string.rep(" ", depth) .. "}"
elseif type(val) == "number" then
tmp = tmp .. tostring(val)
elseif type(val) == "string" then
tmp = tmp .. string.format("%q", val)
elseif type(val) == "boolean" then
tmp = tmp .. (val and "true" or "false")
else
tmp = tmp .. "\"[inserializeable datatype:" .. type(val) .. "]\""
end
return tmp
end
function invertTable(t)
local s={}
for k,v in pairs(t) do
s[v]=k
end
return s
end
--List
List = {}
function List.new ()
return {first = 0, last = -1}
end
function List.pushleft (list, value)
local first = list.first - 1
list.first = first
list[first] = value
end
function List.pushright (list, value)
local last = list.last + 1
list.last = last
list[last] = value
end
function List.popleft (list)
local first = list.first
if first > list.last then error("list is empty") end
local value = list[first]
list[first] = nil -- to allow garbage collection
list.first = first + 1
return value
end
function List.popright (list)
local last = list.last
if list.first > last then error("list is empty") end
local value = list[last]
list[last] = nil -- to allow garbage collection
list.last = last - 1
return value
end
function List.fromTable(t)
local l = List.new()
for i,v in ipairs(t) do
List.pushright(l, v)
end
return l
end
function copyTable(obj, seen)
if type(obj) ~= 'table' then return obj end
if seen and seen[obj] then return seen[obj] end
local s = seen or {}
local res = setmetatable({}, getmetatable(obj))
s[obj] = res
for k, v in pairs(obj) do res[copyTable(k, s)] = copyTable(v, s) end
return res
end
-- Game globals
local currentSpeed = config.normalSpeed
local secondsSinceLastMovement = 0
local lastUpdateExecutionSeconds = nil
local grid = {}
local drawGrid = {}
setmetatable(drawGrid,{__index=grid} )
local gridActor
function pos(x,y) return {x=x,y=y} end
local blocksByPiece = {
I = {pos(0,0),pos(0,1),pos(0,2),pos(0,3), center=pos(0,2)},
J = {pos(0,0),pos(1,0),pos(1,1),pos(1,2), center=pos(1,1)},
L = {pos(0,0),pos(1,0),pos(0,1),pos(0,2), center=pos(1,1)},
T = {pos(0,0),pos(0,1),pos(0,2),pos(1,1), center=pos(0,1)},
Z = {pos(0,0),pos(0,1),pos(1,1),pos(1,2), center=pos(1,1)},
S = {pos(1,0),pos(0,1),pos(1,1),pos(0,2), center=pos(1,1)},
O = {pos(0,0),pos(0,1),pos(1,0),pos(1,1), center=pos(1,0)},
}
local pieceNames = tableKeys(blocksByPiece)
local pieceQueue = List.fromTable(shuffle(pieceNames))
local currentPiece = { name="L", rotation=1, offset={x=0,y=0} }
--Tetris utility functions/tables, and game globals
function popPiece()
currentPiece = { name=List.popright(pieceQueue), rotation=1, offset={x=0,y=0} }
List.pushright(pieceQueue, pieceNames[math.random(#pieceNames)])
return currentPiece
end
popPiece() -- randomize the first piece
function rotateOnce(coord, center)
local rel = {x=coord.x-center.x, y=coord.y-center.y}
return {x=center.x+center.y-coord.y, y=center.y+coord.x-center.x}
end
function rotatePos(coord, numberino, center)
local rel = {x=coord.x-center.x, y=coord.y-center.y}
if numberino == 0 then
return {x=coord.x, y=coord.y}
elseif numberino == 2 then
return rotateOnce(rotateOnce(coord,center),center)
elseif numberino == 1 then
return rotateOnce(coord,center)
end
return rotateOnce(rotateOnce(rotateOnce(coord,center),center),center)
end
function setSpeed(newSpeed)
secondsSinceLastMovement= secondsSinceLastMovement*newSpeed/currentSpeed
currentSpeed = newSpeed
end
function rotatePiece(piece, numberino)
newPiece = {}
for i, v in ipairs(piece) do
newPiece[i] = rotatePos(v, numberino, piece.center)
end
return newPiece
end
function translatePiece(piece, offset)
newPiece = {}
for i, v in ipairs(piece) do
newPiece[i] = v
newPiece[i].y = offset.y + newPiece[i].y
newPiece[i].x = offset.x + newPiece[i].x
end
return newPiece
end
function dropCurrentPiece()
--????
end
local inputMappings = invertTable(config.buttons)
function getPieceBlocks(pieceData)
local pieceBlocks = blocksByPiece[pieceData.name]
pieceBlocks = rotatePiece(pieceBlocks, pieceData.rotation)
return translatePiece(pieceBlocks, {y=pieceData.offset.y, x=pieceData.offset.x+math.floor(config.grid.width/2)})
end
function collidesWithBlocks(piece)
local pieceBlocks = getPieceBlocks(piece)
for i,v in ipairs(pieceBlocks) do
--collision
local x = v.x+1
local y = v.y+1
if grid[x] and grid[x][y] then return true end
--out of bounds
if x > config.grid.width or x < 1 or y > config.grid.height then
return true
end
end
return false
end
function rotateCurrentPiece(rotation)
local newPiece = copyTable(currentPiece)
newPiece.rotation=(currentPiece.rotation+rotation) % 4
if not collidesWithBlocks(newPiece) then
currentPiece = newPiece
return newPiece
end
return nil
end
function moveCurrentPiece(offset)
local newPiece = copyTable(currentPiece)
newPiece.offset.x=newPiece.offset.x+offset.x
newPiece.offset.y=newPiece.offset.y+offset.y
if not collidesWithBlocks(newPiece) then
currentPiece = newPiece
return newPiece
end
return nil
end
local buttonMappings = {
speedUp = function() setSpeed(config.highSpeed) end,
rotateLeft = function() rotateCurrentPiece(1) end,
rotateRight = function() rotateCurrentPiece(-1) end,
rotate180 = function() rotateCurrentPiece(2) end,
drop = function() while tickGravity() do end end,
down = function() setSpeed(config.highSpeed) end,
up = function() setSpeed(config.normalSpeed) end,
left = function() moveCurrentPiece({x=-1,y=0}) end,
right = function() moveCurrentPiece({x=1,y=0}) end,
leftRepeat = function() local b=true while b do b=moveCurrentPiece({x=-1,y=0}) end end,
rightRepeat = function() local b=true while b do b=moveCurrentPiece({x=1,y=0}) end end,
}
-- input callback
function buttonForEvent(event)
local button = inputMappings[string.gsub(event.DeviceInput.button, "DeviceButton_", "")]
if not button then
button = inputMappings[event.char]
end
return button
end
local lastInputs = {}
function buttonPress(button)
lastInputs[button] = os.clock()
buttonMappings[button]()
updateColors()
end
function inputCallback(event)
local button = buttonForEvent(event)
if not button then return end
if event.type == "InputEventType_Release" and button == "down" then
buttonPress("up")
return
elseif event.type ~= "InputEventType_FirstPress" then
if (button == "left" or button == "right") then
if lastInputs[button] and lastInputs[button] > config.inputPollingSeconds then
buttonPress(button.."Repeat")
end
elseif event.type == "InputEventType_Repeat" then
if button == "left" or button == "right" then
buttonMappings[button.."Repeat"]()
end
end
return
end
buttonPress(button)
end
function inputCallback(event)
if event.type == "InputEventType_Release" then
local button = buttonForEvent(event)
if not button then return end
if button == "down" then
buttonPress("up")
end
return
end
if event.type ~= "InputEventType_FirstPress" then
if event.type == "InputEventType_Repeat" then
local button = buttonForEvent(event)
if not button then return end
if button == "left" or button == "right" then
buttonMappings[button.."Repeat"]()
end
end
local button = buttonForEvent(event)
if button and (button == "left" or button == "right") then
if lastInputs[button] and lastInputs[button] > config.inputPollingSeconds then
buttonPress(button.."Repeat")
end
end
return
end
local button = buttonForEvent(event)
if not button then return end
buttonPress(button)
end
-- Update all grid actor quad colors
function updateColors()
drawGrid = {}
if currentPiece.name then
local pieceBlocks = getPieceBlocks(currentPiece)
local pieceColor = config.pieces.colors[currentPiece.name]
for i,v in ipairs(pieceBlocks) do
v.x = v.x+1
v.y=v.y+1
if not drawGrid[v.x] then
if not grid[v.x] then grid[v.x] = {} end
drawGrid[v.x] = {}
setmetatable(drawGrid[v.x], {__index=grid[v.x]})
end
drawGrid[v.x][v.y] = { color = pieceColor}
end
end
setmetatable(drawGrid,{__index=grid} )
MESSAGEMAN:Broadcast("RedrawQuads")
end
function isLineFull(i)
for j=1,config.grid.width do
if not grid[j] or not grid[j][i] or not grid[j][i].color then
return false
end
end
return true
end
function emptyLine(i)
for j=1,config.grid.width do
if grid[j] then
grid[j][i] = nil
end
end
end
function checkFullLines()
for i=1,config.grid.height do
local lineIsFull = isLineFull(i)
if lineIsFull then
emptyLine(i)
for i=i,1,-1 do
for j=1,config.grid.width do
grid[j][i] = grid[j] and grid[j][i-1] or nil
end
end
end
end
end
function tickGravity()
local newPiece = copyTable(currentPiece)
newPiece.offset.y = currentPiece.offset.y+1
if not collidesWithBlocks(newPiece) then
currentPiece = newPiece
return true
else
local pieceBlocks = getPieceBlocks(currentPiece)
local pieceColor = config.pieces.colors[currentPiece.name]
for i,v in ipairs(pieceBlocks) do
v.x = v.x+1
v.y = v.y+1
if not grid[v.x] then grid[v.x] = {} end
grid[v.x][v.y] = {color=pieceColor}
end
popPiece()
checkFullLines()
end
return false
end
-- Tick function (Called every 'currentSpeed' interval, in seconds)
function makeGameTick()
--make the currentPiece fall 1 cell
--check if fall ends (If so set the new piece with -1 offset)
--if the current piece has -1 offset
tickGravity()
updateColors()
end
-- Update function (Called all the time)
local function everyFrame()
if not lastUpdateExecutionSeconds then lastUpdateExecutionSeconds=os.clock() end
secondsSinceLastMovement = secondsSinceLastMovement + os.clock() - lastUpdateExecutionSeconds
lastUpdateExecutionSeconds = os.clock()
if secondsSinceLastMovement > currentSpeed then
secondsSinceLastMovement = secondsSinceLastMovement - currentSpeed
makeGameTick()
end
end
-- grid initialization code
gridActor = Def.ActorFrame{
OnCommand=function(self)
SCREENMAN:GetTopScreen():AddInputCallback(inputCallback)
end,
InitCommand=function(self)
self:SetUpdateFunction(everyFrame)
end
}
for i=1,config.grid.width do
local index = #gridActor+1
gridActor[index] = Def.ActorFrame{ }
grid[i] = {}
for j=1,config.grid.height do
gridActor[index][#(gridActor[index])+1] = Def.Quad {
InitCommand=function(self)
self:xy(SCREEN_WIDTH/2+(i-1-math.floor(config.grid.width/2))*config.grid.blockWidth,(j-1-math.floor(config.grid.height/2))*config.grid.blockHeight+SCREEN_HEIGHT/2):zoomto(config.grid.blockWidth-1,config.grid.blockHeight-1):halign(0):valign(0):diffuse(config.bgColor)
end,
RedrawQuadsMessageCommand = function(self)
self:diffuse(drawGrid[i][j] and drawGrid[i][j].color or config.bgColor)
end
}
end
end
return Def.ActorFrame {gridActor}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment