Files
SE_Exam/lua/script.lua
2025-01-22 00:16:29 +01:00

502 lines
14 KiB
Lua

-- 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 = MAIN_MENU
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 = 1
local yesButton = button.new(300, 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(300, 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("")
nameTextBox:SetBounds(250, 200, 200, 25)
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
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()
GameEngine:fillScreen(Color.new(0, 0, 0))
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 = 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, 250)
if hasGottenLeaderBoard then
local NamesX = 600
local ScoresX = 700
local Y = 100
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:setColor(Color.new(255, 0, 0))
GameEngine:fillScreen(Color.new(0, 0, 0))
GameEngine:drawText("Game Over", 350, 300)
GameEngine:drawText("Press R to restart", 350, 350)
drawScoreBoard()
end
function drawSubmitName()
GameEngine:fillScreen(Color.new(0, 0, 0))
GameEngine:setColor(Color.new(255, 255, 255))
GameEngine:drawText("Name:", 200, 200)
local charwidth = 8
local submitText = "Would you like to submit to the leaderboard"
local submitTextWidth = #submitText * charwidth
GameEngine:drawText(submitText, 400 - submitTextWidth / 2, 50)
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