-- Tetris Game using GameEngine local vector2 = require("vector2") local button = require("button") -- Constants local GRID_WIDTH = 10 local GRID_HEIGHT = 19 local BLOCK_SIZE = 30 local FALL_SPEED = 0.5 local screenWidth = 800 local screenHeight = 600 -- Tetromino shapes and colors local tetrominoes = { { shape = {{1, 1, 1, 1}}, color = Color.new(0, 255, 255) }, -- I { shape = {{1, 1}, {1, 1}}, color = Color.new(255, 255, 0) }, -- O { shape = {{0, 1, 0}, {1, 1, 1}}, color = Color.new(128, 0, 128) }, -- T { shape = {{1, 1, 0}, {0, 1, 1}}, color = Color.new(0, 255, 0) }, -- S { shape = {{0, 1, 1}, {1, 1, 0}}, color = Color.new(255, 0, 0) }, -- Z { shape = {{1, 1, 1}, {1, 0, 0}}, color = Color.new(255, 165, 0) }, -- L { shape = {{1, 1, 1}, {0, 0, 1}}, color = Color.new(0, 0, 255) } -- J } -- Game State local grid = {} local currentPiece = {} local pieceX, pieceY = 4, 0 local fallTimer = 0 local lastKeyState = { Left = false, Right = false, Down = false, Rotate = false, Space = true } -- enum of gameState, Main Menu, Playing, Submitting Name, Game Over local MAIN_MENU = 0 local PLAYING = 1 local GAME_OVER = 2 local SUBMITTING_NAME = 3 local gameState = SUBMITTING_NAME local titleScreenBitmap local nameTextBox local score = 0 -- list of bitmap frames that should be animated local leaderboardFrames = {} local leaderboardFrameTimer = 0 local leaderboardFrameIndex = 1 local leaderboardFreamSpeed = 0.05 local gameOverBitmap local pressRBitmap local enterNameBitmap local yesButton = button.new(350, 400, 100, 50, "Yes", Color.new(0, 255, 0), function() print("Sending Post") local playerName = nameTextBox:GetText() if playerName == "" then playerName = "Anonymous" end local data = "{ \"player\": \"" .. playerName .. "\", \"score\": " .. score .. " }" local response = GameEngine:postRequest("https://api.brammie15.dev/leaderboard/tetris", data) if(response == "Error: Request timed out") then netError = true end print(response) gameState = GAME_OVER end) local noButton = button.new(350, 500, 100, 50, "No", Color.new(255, 0, 0), function() gameState = GAME_OVER end) local hasGottenLeaderBoard = false local leaderboard = {} local netError = false local nextPiece = {} local pieceBag = {} --- region BagStuff local function shuffleBag() pieceBag = {} local indices = {1, 2, 3, 4, 5, 6, 7} for i = #indices, 2, -1 do local j = math.random(i) indices[i], indices[j] = indices[j], indices[i] end for _, index in ipairs(indices) do table.insert(pieceBag, tetrominoes[index]) end end local function getNextPiece() if #pieceBag == 0 then shuffleBag() end local pieceData = table.remove(pieceBag, 1) return { shape = pieceData.shape, color = pieceData.color } end local function getRandomPiece() local pieceData = tetrominoes[math.random(#tetrominoes)] return { shape = pieceData.shape, color = pieceData.color } end --- endregion local function checkCollision(px, py, piece) for y = 1, #piece.shape do for x = 1, #piece.shape[y] do if piece.shape[y][x] == 1 then if px + x < 1 or px + x > GRID_WIDTH or py + y > GRID_HEIGHT or (grid[py + y] and grid[py + y][px + x].value ~= 0) then return true end end end end return false end local function newPiece() currentPiece = nextPiece or getNextPiece() nextPiece = getNextPiece() pieceX, pieceY = 4, 0 if checkCollision(pieceX, pieceY, currentPiece) then gameState = SUBMITTING_NAME end end --- region GhostPieces local function getGhostPieceY() local ghostY = pieceY while not checkCollision(pieceX, ghostY + 1, currentPiece) do ghostY = ghostY + 1 end return ghostY end local function drawGhostPiece() local ghostY = getGhostPieceY() GameEngine:setColor(Color.new(200, 200, 200)) -- Semi-transparent gray for y = 1, #currentPiece.shape do for x = 1, #currentPiece.shape[y] do if currentPiece.shape[y][x] == 1 then GameEngine:fillRect((pieceX + x) * BLOCK_SIZE, (ghostY + y) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE) end end end end --- endregion local function drawNextPiece() GameEngine:setColor(Color.new(255, 255, 255)) GameEngine:drawText("Next:", 650, 100) if nextPiece then for y = 1, #nextPiece.shape do for x = 1, #nextPiece.shape[y] do if nextPiece.shape[y][x] == 1 then GameEngine:setColor(nextPiece.color) GameEngine:fillRect(650 + x * BLOCK_SIZE, 120 + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE) end end end end end local function clearLines() local linesCleared = 0 for y = GRID_HEIGHT, 1, -1 do local full = true for x = 1, GRID_WIDTH do if grid[y][x].value == 0 then full = false break end end if full then table.remove(grid, y) table.insert(grid, 1, {}) for x = 1, GRID_WIDTH do grid[1][x] = { value = 0, color = Color.new(255, 255, 255) } end linesCleared = linesCleared + 1 end end -- Score calculation based on cleared lines local scoreTable = { 100, 300, 500, 800 } if linesCleared > 0 then score = score + scoreTable[linesCleared] or 0 end end local function freezePiece() for y = 1, #currentPiece.shape do for x = 1, #currentPiece.shape[y] do if currentPiece.shape[y][x] == 1 then grid[pieceY + y][pieceX + x] = { value = 1, color = currentPiece.color } end end end clearLines() newPiece() end local function rotatePiece() local newShape = {} for x = 1, #currentPiece.shape[1] do newShape[x] = {} for y = 1, #currentPiece.shape do newShape[x][#currentPiece.shape - y + 1] = currentPiece.shape[y][x] end end if not checkCollision(pieceX, pieceY, { shape = newShape }) then currentPiece.shape = newShape end end local function drawGrid() for y = 1, GRID_HEIGHT do for x = 1, GRID_WIDTH do if grid[y][x].value ~= 0 then GameEngine:setColor(grid[y][x].color) GameEngine:fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE) end GameEngine:setColor(Color.new(50, 50, 50)) GameEngine:drawRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE) end end GameEngine:setColor(Color.new(255, 255, 255)) GameEngine:drawRect(BLOCK_SIZE, BLOCK_SIZE, GRID_WIDTH * BLOCK_SIZE, GRID_HEIGHT * BLOCK_SIZE) end local function drawPiece() GameEngine:setColor(currentPiece.color) for y = 1, #currentPiece.shape do for x = 1, #currentPiece.shape[y] do if currentPiece.shape[y][x] == 1 then GameEngine:fillRect((pieceX + x) * BLOCK_SIZE, (pieceY + y) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE) end end end end function setup_window() GameEngine:setTitle("Tetris") GameEngine:setWidth(screenWidth) GameEngine:setHeight(screenHeight) GameEngine:setFrameRate(60) end --- the set_keylist function --- @return string function set_keylist() return "WASD " end function start() -- Initialize grid for y = 1, GRID_HEIGHT do grid[y] = {} for x = 1, GRID_WIDTH do grid[y][x] = { value = 0, color = Color.new(255, 255, 255) } end end nextPiece = getRandomPiece() newPiece() nameTextBox = Textbox.new("") -- Center on screen local width = 175 local height = 25 nameTextBox:SetBounds(math.floor((screenWidth - width) / 2) - 10, 300, width, height) nameTextBox:Hide() titleScreenBitmap = Bitmap.new("resources/tetrisLogo.bmp", true) -- load frame0 - 4 for i = 0, 4 do print("loading frame" .. i) local bmp = Bitmap.new("resources/leaderboard/frame" .. i .. ".bmp", true) bmp:SetTransparencyColor(Color.new(255, 0, 255)) table.insert(leaderboardFrames, bmp) end gameOverBitmap = Bitmap.new("resources/leaderboard/game_over.bmp", true) gameOverBitmap:SetTransparencyColor(Color.new(255, 0, 255)) pressRBitmap = Bitmap.new("resources/leaderboard/press_r.bmp", true) pressRBitmap:SetTransparencyColor(Color.new(255, 0, 255)) enterNameBitmap = Bitmap.new("resources/leaderboard/enter_name.bmp", true) enterNameBitmap:SetTransparencyColor(Color.new(255, 0, 255)) end function update() -- print(GameEngine:getMouseX(), GameEngine:getMouseY()) if gameState == PLAYING then fallTimer = fallTimer + 1 / 60 if fallTimer >= FALL_SPEED then if not checkCollision(pieceX, pieceY + 1, currentPiece) then pieceY = pieceY + 1 else freezePiece() end fallTimer = 0 end local leftPressed = GameEngine:isKeyDown("A") local rightPressed = GameEngine:isKeyDown("D") local downPressed = GameEngine:isKeyDown("S") local rotatePressed = GameEngine:isKeyDown("W") local spacePressed = GameEngine:isKeyDown(" ") if leftPressed and not lastKeyState.Left and not checkCollision(pieceX - 1, pieceY, currentPiece) then pieceX = pieceX - 1 end if rightPressed and not lastKeyState.Right and not checkCollision(pieceX + 1, pieceY, currentPiece) then pieceX = pieceX + 1 end if downPressed and not lastKeyState.Down and not checkCollision(pieceX, pieceY + 1, currentPiece) then pieceY = pieceY + 1 end if spacePressed and not lastKeyState.Space then while not checkCollision(pieceX, pieceY + 1, currentPiece) do pieceY = pieceY + 1 end freezePiece() end if rotatePressed and not lastKeyState.Rotate then rotatePiece() end lastKeyState.Left = leftPressed lastKeyState.Right = rightPressed lastKeyState.Down = downPressed lastKeyState.Rotate = rotatePressed lastKeyState.Space = spacePressed end if gameState == GAME_OVER then --Update leaderboard gif leaderboardFrameTimer = leaderboardFrameTimer + 1 / 60 if leaderboardFrameTimer >= leaderboardFreamSpeed then leaderboardFrameIndex = leaderboardFrameIndex + 1 if leaderboardFrameIndex > #leaderboardFrames then leaderboardFrameIndex = 1 end leaderboardFrameTimer = 0 end if GameEngine:isKeyDown("R") then gameState = PLAYING score = 0 netError = false hasGottenLeaderBoard = false leaderboard = {} nameTextBox:SetText("") grid = {} for y = 1, GRID_HEIGHT do grid[y] = {} for x = 1, GRID_WIDTH do grid[y][x] = { value = 0, color = Color.new(255, 255, 255) } end end newPiece() end end if gameState == SUBMITTING_NAME then yesButton:update(GameEngine) noButton:update(GameEngine) end if gameState == MAIN_MENU then if GameEngine:isKeyDown(" ") then gameState = PLAYING end end end function drawScoreBoard() if not hasGottenLeaderBoard then local response = GameEngine:getRequest("https://api.brammie15.dev/leaderboard/tetris") if response == "Error: Request timed out" then netError = true end print(response) -- format is -- NAME SCORE if not netError then for line in response:gmatch("[^\r\n]+") do local name, score = line:match("([^%s]+)%s+(%d+)") table.insert(leaderboard, { name = name, score = tonumber(score) }) end table.sort(leaderboard, function(a, b) return a.score > b.score end) end hasGottenLeaderBoard = true end GameEngine:setColor(Color.new(255, 0, 0)) GameEngine:drawText("Score: " .. score, 350, 400) if hasGottenLeaderBoard then local NamesX = 475 local ScoresX = 625 local Y = 150 GameEngine:setColor(Color.new(255, 255, 0)) -- GameEngine:drawText("Leaderboard", 600, 60) GameEngine:drawBitmap(leaderboardFrames[leaderboardFrameIndex], 450, 60) if netError then GameEngine:setColor(Color.new(255, 0, 0)) GameEngine:drawText("Error: Request timed out", 600, 100) GameEngine:drawText("Or server down", 600, 120) GameEngine:drawText("Not gonna check :p", 600, 140) return end for i = 1, math.min(#leaderboard, 20) do GameEngine:setColor(Color.new(255, 255, 255)) GameEngine:drawText(leaderboard[i].name, NamesX, Y) GameEngine:drawText(leaderboard[i].score, ScoresX, Y) Y = Y + 20 end end end function drawGame() GameEngine:fillScreen(Color.new(0, 0, 0)) drawGrid() drawGhostPiece() drawPiece() drawNextPiece() -- Draw the next piece preview GameEngine:setColor(Color.new(255, 255, 255)) GameEngine:drawText("Score: " .. score, 650, 50) end function drawGameOver() nameTextBox:Hide() GameEngine:fillScreen(Color.new(0, 0, 0)) GameEngine:drawBitmap(gameOverBitmap, 0, 0) GameEngine:drawBitmap(pressRBitmap, 0, 0) drawScoreBoard() end function drawSubmitName() GameEngine:fillScreen(Color.new(0, 0, 0)) GameEngine:drawBitmap(enterNameBitmap, 0, 0) nameTextBox:Show() yesButton:draw(GameEngine) noButton:draw(GameEngine) end function drawMainMenu() GameEngine:drawBitmap(titleScreenBitmap, 0, 0) --Press space to start GameEngine:setColor(Color.new(255, 255, 255)) GameEngine:drawText("Press Space to Start", 350, 500) end function draw() if gameState == PLAYING then drawGame() end if gameState == GAME_OVER then drawGameOver() end if gameState == SUBMITTING_NAME then drawSubmitName() end if gameState == MAIN_MENU then drawMainMenu() end end