Created
September 8, 2018 23:39
-
-
Save nico-abram/c3445d1a78b755031a020fbce3eeca12 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
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