Skip to content

Instantly share code, notes, and snippets.

@josefnpat
Last active February 17, 2022 00:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save josefnpat/0765425c21239bf8a9f6 to your computer and use it in GitHub Desktop.
Save josefnpat/0765425c21239bf8a9f6 to your computer and use it in GitHub Desktop.
Simple Tetris Clone for LOVE
function love.conf(t)
t.title = "STC - " .. _VERSION -- The title of the window the game is in (string)
t.author = "Laurens Rodriguez" -- The author of the game (string)
t.identity = "stc" -- The name of the save directory (string)
t.version = "0.9.1" -- The L0VE version this game was made for (number)
t.console = false -- Attach a console (boolean, Windows only)
t.window.width = 480 -- The window width (number)
t.window.height = 272 -- The window height (number)
t.window.fullscreen = false -- Enable fullscreen (boolean)
t.window.vsync = true -- Enable vertical sync (boolean)
t.window.fsaa = 0 -- The number of FSAA-buffers (number)
t.modules.joystick = false -- Enable the joystick module (boolean)
t.modules.audio = true -- Enable the audio module (boolean)
t.modules.keyboard = true -- Enable the keyboard module (boolean)
t.modules.event = true -- Enable the event module (boolean)
t.modules.image = true -- Enable the image module (boolean)
t.modules.graphics = true -- Enable the graphics module (boolean)
t.modules.timer = true -- Enable the timer module (boolean)
t.modules.mouse = false -- Enable the mouse module (boolean)
t.modules.sound = true -- Enable the sound module (boolean)
t.modules.physics = false -- Enable the physics module (boolean)
end
-- ========================================================================== --
-- Game logic implementation. --
-- Copyright (c) 2011 Laurens Rodriguez Oscanoa. --
-- -------------------------------------------------------------------------- --
-- Initial time delay (in milliseconds) between falling moves.
local INIT_DELAY_FALL = 1000;
-- Score points given by filled rows (we use the original NES * 10)
-- http://tetris.wikia.com/wiki/Scoring
local SCORE_1_FILLED_ROW = 400;
local SCORE_2_FILLED_ROW = 1000;
local SCORE_3_FILLED_ROW = 3000;
local SCORE_4_FILLED_ROW = 12000;
-- The player gets points every time he accelerates downfall.
-- The added points are equal to SCORE_2_FILLED_ROW divided by this value.
local SCORE_MOVE_DOWN_DIVISOR = 1000;
-- The player gets points every time he does a hard drop.
-- The added points are equal to SCORE_2_FILLED_ROW divided by these
-- values. If the player is not using the shadow he gets more points.
local SCORE_DROP_DIVISOR = 20;
local SCORE_DROP_WITH_SHADOW_DIVISOR = 100;
-- Number of filled rows required to increase the game level.
local FILLED_ROWS_FOR_LEVEL_UP = 10;
-- The falling delay is multiplied and divided by
-- these factors with every level up.
local DELAY_FACTOR_FOR_LEVEL_UP = 9;
local DELAY_DIVISOR_FOR_LEVEL_UP = 10;
-- Delayed autoshift initial delay.
local DAS_DELAY_TIMER = 200;
-- Delayed autoshift timer for left and right moves.
local DAS_MOVE_TIMER = 40;
-- Rotation auto-repeat delay.
local ROTATION_AUTOREPEAT_DELAY = 375;
-- Rotation autorepeat timer.
local ROTATION_AUTOREPEAT_TIMER = 200;
-- Number of tetromino types.
local TETROMINO_TYPES = 7;
Game = {
-- Playfield size (in tiles).
BOARD_TILEMAP_WIDTH = 10;
BOARD_TILEMAP_HEIGHT = 22;
-- Error codes.
Error = {
NONE = 0, -- Everything is OK, oh wonders!
PLAYER_QUITS = 1, -- The user quits, our fail
NO_MEMORY = -1, -- Not enough memory
NO_VIDEO = -2, -- Video system was not initialized
NO_IMAGES = -3, -- Problem loading the image files
PLATFORM = -4, -- Problem creating platform
ASSERT = -100 -- Something went very very wrong...
};
-- Game events.
Event = {
NONE = 0,
MOVE_DOWN = 1,
MOVE_LEFT = 2,
MOVE_RIGHT = 4,
ROTATE_CW = 8, -- rotate clockwise
ROTATE_CCW = 16, -- rotate counter-clockwise
DROP = 32,
PAUSE = 64,
RESTART = 128,
SHOW_NEXT = 256, -- toggle show next tetromino
SHOW_SHADOW = 512, -- toggle show shadow
QUIT = 1024 -- finish the game
};
-- We are going to store the tetromino cells in a square matrix
-- of this size (this is the size of the biggest tetromino).
TETROMINO_SIZE = 4;
-- Tetromino definitions.
-- They are indexes and must be between: 0 - [TETROMINO_TYPES - 1]
-- http://tetris.wikia.com/wiki/Tetromino
-- Initial cell disposition is commented below.
TetrominoType = {
-- ....
-- ####
-- ....
-- ....
I = 0,
-- ##..
-- ##..
-- ....
-- ....
O = 1,
-- .#..
-- ###.
-- ....
-- ....
T = 2,
-- .##.
-- ##..
-- ....
-- ....
S = 3,
-- ##..
-- .##.
-- ....
-- ....
Z = 4,
-- #...
-- ###.
-- ....
-- ....
J = 5,
-- ..#.
-- ###.
-- ....
-- ....
L = 6
};
-- Color indexes.
Cell = {
EMPTY = -1, -- This value used for empty tiles.
CYAN = 1,
RED = 2,
BLUE = 3,
ORANGE = 4,
GREEN = 5,
YELLOW = 6,
PURPLE = 7,
WHITE = 0 -- Used for effects (if any)
};
COLORS = 8;
-- Create data structure that holds information about our tetromino blocks.
createTetromino = function()
local tetromino = {
cells = {}; -- Tetromino buffer
x = 0;
y = 0;
size = 0;
type = nil;
};
return tetromino;
end;
-- Create data structure for statistical data.
createStatics = function()
local stats = {
score = 0; -- user score for current game
lines = 0; -- total number of lines cleared
totalPieces = 0; -- total number of tetrominoes used
level = 0; -- current game level
pieces = {}; -- number of tetrominoes per type
};
return stats;
end;
-- Game events are stored in bits in this variable.
-- It must be cleared to Game.Event.NONE after being used.
m_events = nil;
-- Matrix that holds the cells (tilemap)
m_map = nil;
m_stats = nil; -- statistic data
m_fallingBlock = nil; -- current falling tetromino
m_nextBlock = nil; -- next tetromino
m_stateChanged = nil; -- true if game state has changed
m_errorCode = nil; -- stores current error code
m_isPaused = nil; -- true if the game is over
m_isOver = nil; -- true if the game is over
m_showPreview = nil; -- true if we must show the preview block
m_showShadow = nil; -- true if we must show the shadow block
m_shadowGap = nil; -- distance between falling block and shadow
m_systemTime = nil; -- system time in milliseconds
m_fallingDelay = nil; -- delay time for falling tetrominoes
m_lastFallTime = nil; -- last time the falling tetromino dropped
-- For delayed autoshift: http://tetris.wikia.com/wiki/DAS
m_delayLeft = nil;
m_delayRight = nil;
m_delayDown = nil;
m_delayRotation = nil;
};
-- The platform must call this method after processing a changed state.
function Game:onChangeProcessed() self.m_stateChanged = false; end
-- Return the cell at the specified position.
function Game:getCell(column, row) return self.m_map[column][row]; end
-- Return true if the game state has changed, false otherwise.
function Game:hasChanged() return self.m_stateChanged; end
-- Return a reference to the game statistic data.
function Game:stats() return self.m_stats; end
-- Return current falling tetromino.
function Game:fallingBlock() return self.m_fallingBlock; end
-- Return next tetromino.
function Game:nextBlock() return self.m_nextBlock; end
-- Return current error code.
function Game:errorCode() return self.m_errorCode; end
-- Return true if the game is paused, false otherwise.
function Game:isPaused() return self.m_isPaused; end
-- Return true if we must show preview tetromino.
function Game:showPreview() return self.m_showPreview; end
-- Return true if we must show ghost shadow.
function Game:showShadow() return self.m_showShadow; end
-- Return height gap between shadow and falling tetromino.
function Game:shadowGap() return self.m_shadowGap; end
-- Set matrix elements to indicated value.
function Game:setMatrixCells(matrix, width, height, value)
for i = 0, width - 1 do
matrix[i] = {};
for j = 0, height - 1 do
matrix[i][j] = value;
end
end
end
-- Initialize tetromino cells for every type of tetromino.
function Game:setTetromino(indexTetromino, tetromino)
-- Initialize tetromino cells to empty cells.
Game:setMatrixCells(tetromino.cells, Game.TETROMINO_SIZE, Game.TETROMINO_SIZE, Game.Cell.EMPTY);
-- Almost all the blocks have size 3.
tetromino.size = Game.TETROMINO_SIZE - 1;
-- Initial configuration from: http://tetris.wikia.com/wiki/SRS
if indexTetromino == Game.TetrominoType.I then
tetromino.cells[0][1] = Game.Cell.CYAN;
tetromino.cells[1][1] = Game.Cell.CYAN;
tetromino.cells[2][1] = Game.Cell.CYAN;
tetromino.cells[3][1] = Game.Cell.CYAN;
tetromino.size = Game.TETROMINO_SIZE;
elseif indexTetromino == Game.TetrominoType.O then
tetromino.cells[0][0] = Game.Cell.YELLOW;
tetromino.cells[0][1] = Game.Cell.YELLOW;
tetromino.cells[1][0] = Game.Cell.YELLOW;
tetromino.cells[1][1] = Game.Cell.YELLOW;
tetromino.size = Game.TETROMINO_SIZE - 2;
elseif indexTetromino == Game.TetrominoType.T then
tetromino.cells[0][1] = Game.Cell.PURPLE;
tetromino.cells[1][0] = Game.Cell.PURPLE;
tetromino.cells[1][1] = Game.Cell.PURPLE;
tetromino.cells[2][1] = Game.Cell.PURPLE;
elseif indexTetromino == Game.TetrominoType.S then
tetromino.cells[0][1] = Game.Cell.GREEN;
tetromino.cells[1][0] = Game.Cell.GREEN;
tetromino.cells[1][1] = Game.Cell.GREEN;
tetromino.cells[2][0] = Game.Cell.GREEN;
elseif indexTetromino == Game.TetrominoType.Z then
tetromino.cells[0][0] = Game.Cell.RED;
tetromino.cells[1][0] = Game.Cell.RED;
tetromino.cells[1][1] = Game.Cell.RED;
tetromino.cells[2][1] = Game.Cell.RED;
elseif indexTetromino == Game.TetrominoType.J then
tetromino.cells[0][0] = Game.Cell.BLUE;
tetromino.cells[0][1] = Game.Cell.BLUE;
tetromino.cells[1][1] = Game.Cell.BLUE;
tetromino.cells[2][1] = Game.Cell.BLUE;
elseif indexTetromino == Game.TetrominoType.L then
tetromino.cells[0][1] = Game.Cell.ORANGE;
tetromino.cells[1][1] = Game.Cell.ORANGE;
tetromino.cells[2][0] = Game.Cell.ORANGE;
tetromino.cells[2][1] = Game.Cell.ORANGE;
end
tetromino.type = indexTetromino;
end
-- Start a new game.
function Game:start()
-- Initialize game data.
self.m_map = {};
self.m_stats = Game:createStatics();
self.m_fallingBlock = Game:createTetromino();
self.m_nextBlock = Game:createTetromino();
self.m_errorCode = Game.Error.NONE;
self.m_systemTime = Platform:getSystemTime();
self.m_lastFallTime = self.m_systemTime;
self.m_isOver = false;
self.m_isPaused = false;
self.m_showPreview = true;
self.m_events = Game.Event.NONE;
self.m_fallingDelay = INIT_DELAY_FALL;
self.m_showShadow = true;
-- Initialize game statistics.
for i = 0, TETROMINO_TYPES - 1 do
self.m_stats.pieces[i] = 0;
end
-- Initialize game tile map.
Game:setMatrixCells(self.m_map, Game.BOARD_TILEMAP_WIDTH, Game.BOARD_TILEMAP_HEIGHT, Game.Cell.EMPTY);
-- Initialize falling tetromino.
Game:setTetromino(Platform:random() % TETROMINO_TYPES, self.m_fallingBlock);
self.m_fallingBlock.x = math.floor((Game.BOARD_TILEMAP_WIDTH - self.m_fallingBlock.size) / 2);
self.m_fallingBlock.y = 0;
-- Initialize preview tetromino.
Game:setTetromino(Platform.random() % TETROMINO_TYPES, self.m_nextBlock);
-- Initialize events.
Game:onTetrominoMoved();
-- Initialize delayed autoshift.
self.m_delayLeft = -1;
self.m_delayRight = -1;
self.m_delayDown = -1;
self.m_delayRotation = -1;
end
-- Initialize the game. The error code (if any) is saved in [mErrorcode].
function Game:init()
-- Initialize platform.
Platform:init();
-- If everything is OK start the game.
Game:start();
end
-- Rotate falling tetromino. If there are no collisions when the
-- tetromino is rotated this modifies the tetromino's cell buffer.
function Game:rotateTetromino(clockwise)
local i; local j;
-- Temporary array to hold rotated cells.
local rotated = {};
-- If TETROMINO_O is falling return immediately.
if (self.m_fallingBlock.type == Game.TetrominoType.O) then
-- Rotation doesn't require any changes.
return;
end
-- Initialize rotated cells to blank.
Game:setMatrixCells(rotated, Game.TETROMINO_SIZE, Game.TETROMINO_SIZE, Game.Cell.EMPTY);
-- Copy rotated cells to the temporary array.
for i = 0, self.m_fallingBlock.size - 1 do
for j = 0, self.m_fallingBlock.size - 1 do
if (clockwise) then
rotated[self.m_fallingBlock.size - j - 1][i] = self.m_fallingBlock.cells[i][j];
else
rotated[j][self.m_fallingBlock.size - i - 1] = self.m_fallingBlock.cells[i][j];
end
end
end
local wallDisplace = 0;
-- Check collision with left wall.
if (self.m_fallingBlock.x < 0) then
i = 0;
while ((wallDisplace == 0) and (i < -self.m_fallingBlock.x)) do
for j = 0, self.m_fallingBlock.size - 1 do
if (rotated[i][j] ~= Game.Cell.EMPTY) then
wallDisplace = i - self.m_fallingBlock.x;
break;
end
end
i = i + 1;
end
-- Or check collision with right wall.
elseif (self.m_fallingBlock.x > Game.BOARD_TILEMAP_WIDTH - self.m_fallingBlock.size) then
i = self.m_fallingBlock.size - 1;
while ((wallDisplace == 0) and (i >= Game.BOARD_TILEMAP_WIDTH - self.m_fallingBlock.x)) do
for j = 0, self.m_fallingBlock.size - 1 do
if (rotated[i][j] ~= Game.Cell.EMPTY) then
wallDisplace = -self.m_fallingBlock.x - i + Game.BOARD_TILEMAP_WIDTH - 1;
break;
end
end
i = i - 1;
end
end
-- Check collision with board floor and other cells on board.
for i = 0, self.m_fallingBlock.size - 1 do
for j = 0, self.m_fallingBlock.size - 1 do
if (rotated[i][j] ~= Game.Cell.EMPTY) then
-- Check collision with bottom border of the map.
if (self.m_fallingBlock.y + j >= Game.BOARD_TILEMAP_HEIGHT) then
-- There is a collision therefore return.
return;
end
-- Check collision with existing cells in the map.
if (self.m_map[i + self.m_fallingBlock.x + wallDisplace][j + self.m_fallingBlock.y] ~= Game.Cell.EMPTY) then
-- There is a collision therefore return.
return;
end
end
end
end
-- Move the falling piece if there was wall collision and it's a legal move.
if (wallDisplace ~= 0) then
self.m_fallingBlock.x = self.m_fallingBlock.x + wallDisplace;
end
-- There are no collisions, replace tetromino cells with rotated cells.
for i = 0, Game.TETROMINO_SIZE - 1 do
for j = 0, Game.TETROMINO_SIZE - 1 do
self.m_fallingBlock.cells[i][j] = rotated[i][j];
end
end
Game:onTetrominoMoved();
end
-- Check if tetromino will collide with something if it is moved in the requested direction.
-- If there are collisions returns 1 else returns 0.
function Game:checkCollision(dx, dy)
local newx = self.m_fallingBlock.x + dx;
local newy = self.m_fallingBlock.y + dy;
for i = 0, self.m_fallingBlock.size - 1 do
for j = 0, self.m_fallingBlock.size - 1 do
if (self.m_fallingBlock.cells[i][j] ~= Game.Cell.EMPTY) then
-- Check that tetromino would be inside the left, right and bottom borders.
if ((newx + i < 0) or (newx + i >= Game.BOARD_TILEMAP_WIDTH)
or (newy + j >= Game.BOARD_TILEMAP_HEIGHT)) then
return true;
end
-- Check that tetromino won't collide with existing cells in the map.
if (self.m_map[newx + i][newy + j] ~= Game.Cell.EMPTY) then
return true;
end
end
end
end
return false;
end
-- Game scoring: http://tetris.wikia.com/wiki/Scoring
function Game:onFilledRows(filledRows)
-- Update total number of filled rows.
self.m_stats.lines = self.m_stats.lines + filledRows;
-- Increase score accordingly to the number of filled rows.
if (filledRows == 1) then
self.m_stats.score = self.m_stats.score + (SCORE_1_FILLED_ROW * (self.m_stats.level + 1));
elseif (filledRows == 2) then
self.m_stats.score = self.m_stats.score + (SCORE_2_FILLED_ROW * (self.m_stats.level + 1));
elseif (filledRows == 3) then
self.m_stats.score = self.m_stats.score + (SCORE_3_FILLED_ROW * (self.m_stats.level + 1));
elseif (filledRows == 4) then
self.m_stats.score = self.m_stats.score + (SCORE_4_FILLED_ROW * (self.m_stats.level + 1));
else
-- This shouldn't happen, but if happens kill the game.
self.m_errorCode = Game.Error.ASSERT;
end
-- Check if we need to update the level.
if (self.m_stats.lines >= FILLED_ROWS_FOR_LEVEL_UP * (self.m_stats.level + 1)) then
self.m_stats.level = self.m_stats.level + 1;
-- Increase speed for falling tetrominoes.
self.m_fallingDelay = math.floor(DELAY_FACTOR_FOR_LEVEL_UP * self.m_fallingDelay
/ DELAY_DIVISOR_FOR_LEVEL_UP);
end
end
-- Move tetromino in the direction specified by (x, y) (in tile units)
-- This function detects if there are filled rows or if the move
-- lands a falling tetromino, also checks for game over condition.
function Game:moveTetromino(x, y)
local i; local j;
-- Check if the move would create a collision.
if (Game:checkCollision(x, y)) then
-- In case of collision check if move was downwards (y == 1)
if (y == 1) then
-- Check if collision occurs when the falling
-- tetromino is on the 1st or 2nd row.
if (self.m_fallingBlock.y <= 1) then
-- If this happens the game is over.
self.m_isOver = true;
else
-- The falling tetromino has reached the bottom,
-- so we copy their cells to the board map.
for i = 0, self.m_fallingBlock.size - 1 do
for j = 0, self.m_fallingBlock.size - 1 do
if (self.m_fallingBlock.cells[i][j] ~= Game.Cell.EMPTY) then
self.m_map[self.m_fallingBlock.x + i][self.m_fallingBlock.y + j]
= self.m_fallingBlock.cells[i][j];
end
end
end
-- Check if the landing tetromino has created full rows.
local numFilledRows = 0;
for j = 1, Game.BOARD_TILEMAP_HEIGHT - 1 do
local hasFullRow = true;
for i = 0, Game.BOARD_TILEMAP_WIDTH - 1 do
if (self.m_map[i][j] == Game.Cell.EMPTY) then
hasFullRow = false;
break;
end
end
-- If we found a full row we need to remove that row from the map
-- we do that by just moving all the above rows one row below.
if (hasFullRow) then
for x = 0, Game.BOARD_TILEMAP_WIDTH - 1 do
for y = j, 1, -1 do
self.m_map[x][y] = self.m_map[x][y - 1];
end
end
-- Increase filled row counter.
numFilledRows = numFilledRows + 1;
end
end
-- Update game statistics.
if (numFilledRows > 0) then
Game:onFilledRows(numFilledRows);
end
self.m_stats.totalPieces = self.m_stats.totalPieces + 1;
self.m_stats.pieces[self.m_fallingBlock.type]
= self.m_stats.pieces[self.m_fallingBlock.type] + 1;
-- Use preview tetromino as falling tetromino.
-- Copy preview tetromino for falling tetromino.
for i = 0, Game.TETROMINO_SIZE - 1 do
for j = 0, Game.TETROMINO_SIZE - 1 do
self.m_fallingBlock.cells[i][j] = self.m_nextBlock.cells[i][j];
end
end
self.m_fallingBlock.size = self.m_nextBlock.size;
self.m_fallingBlock.type = self.m_nextBlock.type;
-- Reset position.
self.m_fallingBlock.y = 0;
self.m_fallingBlock.x = math.floor((Game.BOARD_TILEMAP_WIDTH - self.m_fallingBlock.size) / 2);
Game:onTetrominoMoved();
-- Create next preview tetromino.
Game:setTetromino(Platform:random() % TETROMINO_TYPES, self.m_nextBlock);
end
end
else
-- There are no collisions, just move the tetromino.
self.m_fallingBlock.x = self.m_fallingBlock.x + x;
self.m_fallingBlock.y = self.m_fallingBlock.y + y;
end
Game:onTetrominoMoved();
end
-- Hard drop.
function Game:dropTetromino()
-- Shadow has already calculated the landing position.
self.m_fallingBlock.y = self.m_fallingBlock.y + self.m_shadowGap;
-- Force lock.
Game:moveTetromino(0, 1);
-- Update score.
if (self.m_showShadow) then
self.m_stats.score = self.m_stats.score + (SCORE_2_FILLED_ROW * (self.m_stats.level + 1)
/ SCORE_DROP_WITH_SHADOW_DIVISOR);
else
self.m_stats.score = self.m_stats.score + (SCORE_2_FILLED_ROW * (self.m_stats.level + 1)
/ SCORE_DROP_DIVISOR);
end
end
-- Main game function called every frame.
function Game:update()
-- Update game state.
if self.m_isOver then
if isFlagSet(self.m_events, Game.Event.RESTART) then
self.m_isOver = false;
Game:start();
end
else
local currentTime = Platform:getSystemTime();
-- Process delayed autoshift.
local timeDelta = currentTime - self.m_systemTime;
if (self.m_delayDown > 0) then
self.m_delayDown = self.m_delayDown - timeDelta;
if (self.m_delayDown <= 0) then
self.m_delayDown = DAS_MOVE_TIMER;
self.m_events = setFlag(self.m_events, Game.Event.MOVE_DOWN);
end
end
if (self.m_delayLeft > 0) then
self.m_delayLeft = self.m_delayLeft - timeDelta;
if (self.m_delayLeft <= 0) then
self.m_delayLeft = DAS_MOVE_TIMER;
self.m_events = setFlag(self.m_events, Game.Event.MOVE_LEFT);
end
elseif (self.m_delayRight > 0) then
self.m_delayRight = self.m_delayRight - timeDelta;
if (self.m_delayRight <= 0) then
self.m_delayRight = DAS_MOVE_TIMER;
self.m_events = setFlag(self.m_events, Game.Event.MOVE_RIGHT);
end
end
if (self.m_delayRotation > 0) then
self.m_delayRotation = self.m_delayRotation - timeDelta;
if (self.m_delayRotation <= 0) then
self.m_delayRotation = ROTATION_AUTOREPEAT_TIMER;
self.m_events = setFlag(self.m_events, Game.Event.ROTATE_CW);
end
end
-- Always handle pause event.
if isFlagSet(self.m_events, Game.Event.PAUSE) then
self.m_isPaused = not self.m_isPaused;
self.m_events = Game.Event.NONE;
end
-- Check if the game is paused.
if (self.m_isPaused) then
-- We achieve the effect of pausing the game
-- adding the last frame duration to lastFallTime.
self.m_lastFallTime = self.m_lastFallTime + (currentTime - self.m_systemTime);
else
if (self.m_events ~= Game.Event.NONE) then
if isFlagSet(self.m_events, Game.Event.SHOW_NEXT) then
self.m_showPreview = not self.m_showPreview;
self.m_stateChanged = true;
end
if isFlagSet(self.m_events, Game.Event.SHOW_SHADOW) then
self.m_showShadow = not self.m_showShadow;
self.m_stateChanged = true;
end
if isFlagSet(self.m_events, Game.Event.DROP) then
Game:dropTetromino();
end
if isFlagSet(self.m_events, Game.Event.ROTATE_CW) then
Game:rotateTetromino(true);
end
if isFlagSet(self.m_events, Game.Event.MOVE_RIGHT) then
Game:moveTetromino(1, 0);
elseif isFlagSet(self.m_events, Game.Event.MOVE_LEFT) then
Game:moveTetromino(-1, 0);
end
if isFlagSet(self.m_events, Game.Event.MOVE_DOWN) then
-- Update score if the player accelerates downfall.
self.m_stats.score = self.m_stats.score + (SCORE_2_FILLED_ROW * (self.m_stats.level + 1)
/ SCORE_MOVE_DOWN_DIVISOR);
Game:moveTetromino(0, 1);
end
self.m_events = Game.Event.NONE;
end
-- Check if it's time to move downwards the falling tetromino.
if (currentTime - self.m_lastFallTime >= self.m_fallingDelay) then
Game:moveTetromino(0, 1);
self.m_lastFallTime = currentTime;
end
end
-- Save current time for next game update.
self.m_systemTime = currentTime;
end
end
-- This event is called when the falling tetromino is moved.
function Game:onTetrominoMoved()
local y = 1;
-- Calculate number of cells where shadow tetromino would be.
while (not Game:checkCollision(0, y)) do
y = y + 1;
end
self.m_shadowGap = y - 1;
self.m_stateChanged = true;
end
-- Process a key down event.
function Game:onEventStart(command)
if (command == Game.Event.QUIT) then
self.m_errorCode = Error.PLAYER_QUITS;
elseif (command == Game.Event.MOVE_DOWN) then
self.m_events = setFlag(self.m_events, Game.Event.MOVE_DOWN);
self.m_delayDown = DAS_DELAY_TIMER;
elseif (command == Game.Event.ROTATE_CW) then
self.m_events = setFlag(self.m_events, Game.Event.ROTATE_CW);
self.m_delayRotation = ROTATION_AUTOREPEAT_DELAY;
elseif (command == Game.Event.MOVE_LEFT) then
self.m_events = setFlag(self.m_events, Game.Event.MOVE_LEFT);
self.m_delayLeft = DAS_DELAY_TIMER;
elseif (command == Game.Event.MOVE_RIGHT) then
self.m_events = setFlag(self.m_events, Game.Event.MOVE_RIGHT);
self.m_delayRight = DAS_DELAY_TIMER;
elseif (command == Game.Event.DROP)
or (command == Game.Event.RESTART)
or (command == Game.Event.PAUSE)
or (command == Game.Event.SHOW_NEXT)
or (command == Game.Event.SHOW_SHADOW) then
self.m_events = setFlag(self.m_events, command);
end
end
-- Process a key up event.
function Game:onEventEnd(command)
if (command == Game.Event.MOVE_DOWN) then
self.m_delayDown = -1;
elseif (command == Game.Event.MOVE_LEFT) then
self.m_delayLeft = -1;
elseif (command == Game.Event.MOVE_RIGHT) then
self.m_delayRight = -1;
elseif (command == Game.Event.ROTATE_CW) then
self.m_delayRotation = -1;
end
end
-- Bit flags utility helpers.
function isFlagSet(set, flag)
return (set % (2*flag) >= flag);
end
function setFlag(set, flag)
if (set % (2*flag) >= flag) then
return set;
end
return (set + flag);
end
function clearFlag(set, flag)
if (set % (2*flag) >= flag) then
return (set - flag);
end
return set;
end
-- ========================================================================== --
-- STC - SIMPLE TETRIS CLONE --
-- -------------------------------------------------------------------------- --
-- A simple tetris clone in Lua using the LOVE engine: --
-- http://love2d.org/ --
-- --
-- -------------------------------------------------------------------------- --
-- Copyright (c) 2011 Laurens Rodriguez Oscanoa. --
-- --
-- Permission is hereby granted, free of charge, to any person --
-- obtaining a copy of this software and associated documentation --
-- files (the "Software"), to deal in the Software without --
-- restriction, including without limitation the rights to use, --
-- copy, modify, merge, publish, distribute, sublicense, and/or sell --
-- copies of the Software, and to permit persons to whom the --
-- Software is furnished to do so, subject to the following --
-- conditions: --
-- --
-- The above copyright notice and this permission notice shall be --
-- included in all copies or substantial portions of the Software. --
-- --
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, --
-- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES --
-- OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND --
-- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT --
-- HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, --
-- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING --
-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR --
-- OTHER DEALINGS IN THE SOFTWARE. --
-- -------------------------------------------------------------------------- --
require("game");
require("platform")
function love.load()
Game:init();
end
function love.update(dt)
Game:update();
end
function love.draw()
Platform:renderGame();
end
function love.keypressed(key, unicode)
Platform:onKeyDown(key);
end
function love.keyreleased(key)
Platform:onKeyUp(key);
end
-- ========================================================================== --
-- Platform implementation. --
-- Copyright (c) 2011 Laurens Rodriguez Oscanoa. --
-- -------------------------------------------------------------------------- --
-- Screen size
local SCREEN_WIDTH = 480;
local SCREEN_HEIGHT = 272;
-- Size of square tile
local TILE_SIZE = 12;
-- Board up-left corner coordinates
local BOARD_X = 180;
local BOARD_Y = 4;
-- Preview tetromino position
local PREVIEW_X = 112;
local PREVIEW_Y = 210;
-- Score position and length on screen
local SCORE_X = 72;
local SCORE_Y = 52;
local SCORE_LENGTH = 10;
-- Lines position and length on screen
local LINES_X = 108;
local LINES_Y = 34;
local LINES_LENGTH = 5;
-- Level position and length on screen
local LEVEL_X = 108;
local LEVEL_Y = 16;
local LEVEL_LENGTH = 5;
-- Tetromino subtotals position
local TETROMINO_X = 425;
local TETROMINO_L_Y = 53;
local TETROMINO_I_Y = 77;
local TETROMINO_T_Y = 101;
local TETROMINO_S_Y = 125;
local TETROMINO_Z_Y = 149;
local TETROMINO_O_Y = 173;
local TETROMINO_J_Y = 197;
-- Size of subtotals
local TETROMINO_LENGTH = 5;
-- Tetromino total position
local PIECES_X = 418;
local PIECES_Y = 221;
local PIECES_LENGTH = 6;
-- Size of number
local NUMBER_WIDTH = 7;
local NUMBER_HEIGHT = 9;
Platform = {
m_bmpBackground = nil;
m_bmpBlocks = nil;
m_bmpNumbers = nil;
m_blocks = nil;
m_numbers = nil;
m_musicLoop = nil;
m_musicIntro = nil;
m_musicMute = nil;
};
-- Initializes platform.
function Platform:init()
-- Initialize random generator
math.randomseed(os.time());
-- Load images.
self.m_bmpBackground = love.graphics.newImage("back.png");
self.m_bmpBlocks = love.graphics.newImage("blocks.png");
self.m_bmpBlocks:setFilter("nearest", "nearest");
local w = self.m_bmpBlocks:getWidth();
local h = self.m_bmpBlocks:getHeight();
-- Load music.
self.m_musicIntro = love.audio.newSource("stc_theme_intro.ogg");
self.m_musicIntro:setVolume(0.5);
self.m_musicIntro:play();
self.m_musicLoop = love.audio.newSource("stc_theme_loop.ogg", "stream");
self.m_musicLoop:setLooping(true);
self.m_musicLoop:setVolume(0.5);
m_musicMute = false;
-- Create quads for blocks
self.m_blocks = {};
for shadow = 0, 1 do
self.m_blocks[shadow] = {};
for color = 0, Game.COLORS - 1 do
self.m_blocks[shadow][color] = love.graphics.newQuad(TILE_SIZE * color, (TILE_SIZE + 1) * shadow,
TILE_SIZE + 1, TILE_SIZE + 1, w, h);
end
end
self.m_bmpNumbers = love.graphics.newImage("numbers.png");
self.m_bmpNumbers:setFilter("nearest", "nearest");
w = self.m_bmpNumbers:getWidth();
h = self.m_bmpNumbers:getHeight();
-- Create quads for numbers
self.m_numbers = {};
for color = 0, Game.COLORS - 1 do
self.m_numbers[color] = {};
for digit = 0, 9 do
self.m_numbers[color][digit] = love.graphics.newQuad(NUMBER_WIDTH * digit, NUMBER_HEIGHT * color,
NUMBER_WIDTH, NUMBER_HEIGHT, w, h);
end
end
end
-- Process events and notify game.
function Platform:onKeyDown(key)
if (key == "escape") then
love.event.push("quit");
end
if ((key == "left") or (key == "a") or (key == "h")) then
Game:onEventStart(Game.Event.MOVE_LEFT);
end
if ((key == "right") or (key == "d") or (key == "l")) then
Game:onEventStart(Game.Event.MOVE_RIGHT);
end
if ((key == "down") or (key == "s") or (key == "j")) then
Game:onEventStart(Game.Event.MOVE_DOWN);
end
if ((key == "up") or (key == "w") or (key == "k")) then
Game:onEventStart(Game.Event.ROTATE_CW);
end
if (key == " ") then
Game:onEventStart(Game.Event.DROP);
end
if (key == "f5") then
Game:onEventStart(Game.Event.RESTART);
end
if (key == "f1") then
Game:onEventStart(Game.Event.PAUSE);
end
if (key == "f2") then
Game:onEventStart(Game.Event.SHOW_NEXT);
end
if (key == "f3") then
Game:onEventStart(Game.Event.SHOW_SHADOW);
end
end
function Platform:onKeyUp(key)
if ((key == "left") or (key == "a")) then
Game:onEventEnd(Game.Event.MOVE_LEFT);
end
if ((key == "right") or (key == "d")) then
Game:onEventEnd(Game.Event.MOVE_RIGHT);
end
if ((key == "down") or (key == "s")) then
Game:onEventEnd(Game.Event.MOVE_DOWN);
end
if ((key == "up") or (key == "w")) then
Game:onEventEnd(Game.Event.ROTATE_CW);
end
if (key == "f4") then
if (self.m_musicMute) then
if (self.m_musicIntro) then
self.m_musicIntro:resume();
else
self.m_musicLoop:resume();
end
else
if (self.m_musicIntro) then
self.m_musicIntro:pause();
else
self.m_musicLoop:pause();
end
end
self.m_musicMute = not self.m_musicMute;
end
end
-- Draw a tile from a tetromino
function Platform:drawTile(x, y, tile, shadow)
love.graphics.draw(self.m_bmpBlocks, self.m_blocks[shadow][tile], x, y);
end
-- Draw a number on the given position
function Platform:drawNumber(x, y, number, length, color)
local pos = 0;
repeat
love.graphics.draw(self.m_bmpNumbers, self.m_numbers[color][number % 10],
x + NUMBER_WIDTH * (length - pos), y);
number = math.floor(number / 10);
pos = pos + 1;
until (pos >= length);
end
-- Render the state of the game using platform functions.
function Platform:renderGame()
-- Draw background
love.graphics.draw(self.m_bmpBackground, 0, 0);
-- Draw preview block
if Game:showPreview() then
for i = 0, Game.TETROMINO_SIZE - 1 do
for j = 0, Game.TETROMINO_SIZE - 1 do
if (Game:nextBlock().cells[i][j] ~= Game.Cell.EMPTY) then
Platform:drawTile(PREVIEW_X + TILE_SIZE * i,
PREVIEW_Y + TILE_SIZE * j,
Game:nextBlock().cells[i][j], 0);
end
end
end
end
-- Draw shadow tetromino
if (Game:showShadow() and Game:shadowGap() > 0) then
for i = 0, Game.TETROMINO_SIZE - 1 do
for j = 0, Game.TETROMINO_SIZE - 1 do
if (Game:fallingBlock().cells[i][j] ~= Game.Cell.EMPTY) then
Platform:drawTile(BOARD_X + (TILE_SIZE * (Game:fallingBlock().x + i)),
BOARD_Y + (TILE_SIZE * (Game:fallingBlock().y + Game:shadowGap() + j)),
Game:fallingBlock().cells[i][j], 1);
end
end
end
end
-- Draw the cells in the board
for i = 0, Game.BOARD_TILEMAP_WIDTH - 1 do
for j = 0, Game.BOARD_TILEMAP_HEIGHT - 1 do
if (Game:getCell(i, j) ~= Game.Cell.EMPTY) then
Platform:drawTile(BOARD_X + (TILE_SIZE * i),
BOARD_Y + (TILE_SIZE * j),
Game:getCell(i, j), 0);
end
end
end
-- Draw falling tetromino
for i = 0, Game.TETROMINO_SIZE - 1 do
for j = 0, Game.TETROMINO_SIZE - 1 do
if (Game:fallingBlock().cells[i][j] ~= Game.Cell.EMPTY) then
Platform:drawTile(BOARD_X + TILE_SIZE * (Game:fallingBlock().x + i),
BOARD_Y + TILE_SIZE * (Game:fallingBlock().y + j),
Game:fallingBlock().cells[i][j], 0);
end
end
end
-- Draw game statistic data
if (not Game:isPaused()) then
Platform:drawNumber(LEVEL_X, LEVEL_Y, Game:stats().level, LEVEL_LENGTH, Game.Cell.WHITE);
Platform:drawNumber(LINES_X, LINES_Y, Game:stats().lines, LINES_LENGTH, Game.Cell.WHITE);
Platform:drawNumber(SCORE_X, SCORE_Y, Game:stats().score, SCORE_LENGTH, Game.Cell.WHITE);
Platform:drawNumber(TETROMINO_X, TETROMINO_L_Y, Game:stats().pieces[Game.TetrominoType.L], TETROMINO_LENGTH, Game.Cell.ORANGE);
Platform:drawNumber(TETROMINO_X, TETROMINO_I_Y, Game:stats().pieces[Game.TetrominoType.I], TETROMINO_LENGTH, Game.Cell.CYAN);
Platform:drawNumber(TETROMINO_X, TETROMINO_T_Y, Game:stats().pieces[Game.TetrominoType.T], TETROMINO_LENGTH, Game.Cell.PURPLE);
Platform:drawNumber(TETROMINO_X, TETROMINO_S_Y, Game:stats().pieces[Game.TetrominoType.S], TETROMINO_LENGTH, Game.Cell.GREEN);
Platform:drawNumber(TETROMINO_X, TETROMINO_Z_Y, Game:stats().pieces[Game.TetrominoType.Z], TETROMINO_LENGTH, Game.Cell.RED);
Platform:drawNumber(TETROMINO_X, TETROMINO_O_Y, Game:stats().pieces[Game.TetrominoType.O], TETROMINO_LENGTH, Game.Cell.YELLOW);
Platform:drawNumber(TETROMINO_X, TETROMINO_J_Y, Game:stats().pieces[Game.TetrominoType.J], TETROMINO_LENGTH, Game.Cell.BLUE);
Platform:drawNumber(PIECES_X, PIECES_Y, Game:stats().totalPieces, PIECES_LENGTH, Game.Cell.WHITE);
end
-- Adding music loop check here for convenience.
if (self.m_musicIntro) then
if (self.m_musicIntro:isStopped()) then
self.m_musicIntro = nil;
self.m_musicLoop:play();
end
end
end
function Platform:getSystemTime()
return math.floor(1000 * love.timer.getTime());
end
function Platform:random()
return math.random(1000000000);
end
This file has been truncated, but you can view the full file.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment