Skip to content

Instantly share code, notes, and snippets.

@nico-abram
Created September 8, 2018 21:05
Show Gist options
  • Save nico-abram/fe8a1b9bf49b491023c7340347933080 to your computer and use it in GitHub Desktop.
Save nico-abram/fe8a1b9bf49b491023c7340347933080 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("#33FF33CC"),
normalSpeed = 2,
highSpeed = 0.01,
inputPollingSeconds = 0.05,
}
-- 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 currentPiece = { name="L", rotation=1, offset={x=0,y=0} }
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)},
J = {pos(0,0),pos(1,0),pos(1,1),pos(1,2)},
L = {pos(0,0),pos(1,0),pos(0,1),pos(0,2)},
T = {pos(0,0),pos(0,1),pos(0,2),pos(1,1)},
Z = {pos(0,0),pos(0,1),pos(1,1),pos(1,2)},
S = {pos(1,0),pos(0,1),pos(1,1),pos(0,2)},
O = {pos(0,0),pos(0,1),pos(1,0),pos(1,1)},
}
local pieceNames = tableKeys(blocksByPiece)
local pieceQueue = List.fromTable(shuffle(pieceNames))
--Tetris utility functions/tables, and game globals
function rotatePos(coord, numberino)
if numberino == 0 then
return {x=coord.x, y=coord.y}
elseif numberino == 1 then
return {x=coord.y, y=coord.x}
elseif numberino == 2 then
return {x=-1*coord.x+1, y=coord.y}
end
return {x=-1*coord.y+1, y=coord.x}
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)
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
if grid[v.x] and grid[v.x][v.y] then return true end
--out of bounds
if v.x > config.grid.width-1 or v.x < 0 or v.y > config.grid.height-1 or v.y < 0 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 = dropCurrentPiece,
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)
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
-- 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
currentPiece.offset.y = currentPiece.offset.y+1
local newPiece = copyTable(currentPiece)
newPiece.offset.y = currentPiece.offset.y+1
if not collidesWithBlocks(newPiece) then
currentPiece = newPiece
else
local pieceBlocks = getPieceBlocks(currentPiece)
local pieceColor = config.pieces.colors[currentPiece.name]
for i,v in ipairs(pieceBlocks) do
if not grid[v.x] then grid[v.x] = {} end
grid[v.x][v.y] = {color=pieceColor}
end
currentPiece = { name=List.popright(pieceQueue), rotation=1, offset={x=0,y=0} }
List.pushright(pieceQueue, pieceNames[math.random(#pieceNames)])
end
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