From 208f40d00518b213465175ea791112de9c0a1790 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 30 Sep 2023 22:14:06 -0700 Subject: [PATCH 1/4] Convert fileendings to UNIX style --- README.md | 14 +- main.lua | 2 +- pdxinfo | 2 +- scene.lua | 92 ++-- stage.lua | 1464 ++++++++++++++++++++++++------------------------- tetromino.lua | 658 +++++++++++----------- 6 files changed, 1116 insertions(+), 1116 deletions(-) diff --git a/README.md b/README.md index 14a1ef8..2cbf897 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# Rainblocks - -A falling tetromino game for the Playdate. - -Music by [Holizna](https://freemusicarchive.org/music/holiznacc0/lo-fi-and-chill) - -Sound effects by [JDWasabi](https://jdwasabi.itch.io/8-bit-16-bit-sound-effects-pack), [ColorAlpha](https://coloralpha.itch.io/50-menu-interface-sfx), and [ObsydianX](https://obsydianx.itch.io/interface-sfx-pack-1) \ No newline at end of file +# Rainblocks + +A falling tetromino game for the Playdate. + +Music by [Holizna](https://freemusicarchive.org/music/holiznacc0/lo-fi-and-chill) + +Sound effects by [JDWasabi](https://jdwasabi.itch.io/8-bit-16-bit-sound-effects-pack), [ColorAlpha](https://coloralpha.itch.io/50-menu-interface-sfx), and [ObsydianX](https://obsydianx.itch.io/interface-sfx-pack-1) diff --git a/main.lua b/main.lua index 66e7b04..2483fce 100644 --- a/main.lua +++ b/main.lua @@ -160,4 +160,4 @@ end playdate.deviceWillSleep = function() currentStage:saveData() -end \ No newline at end of file +end diff --git a/pdxinfo b/pdxinfo index f47ec47..b5399ba 100644 --- a/pdxinfo +++ b/pdxinfo @@ -3,4 +3,4 @@ author=Zenzoa description=A chill falling blocks game for the Playdate. bundleID=com.zenzoa.rainblocks version=1.0.1 -imagePath=images \ No newline at end of file +imagePath=images diff --git a/scene.lua b/scene.lua index f8f929a..767c02d 100644 --- a/scene.lua +++ b/scene.lua @@ -1,46 +1,46 @@ -local scene = { - create = function() - local s = { - bgImageTable = Gfx.imagetable.new('images/bg.gif'), - bgImageIndex = 1, - skyImage = Gfx.image.new('images/sky.png'), - clouds1Image = Gfx.image.new('images/clouds1.png'), - clouds2Image = Gfx.image.new('images/clouds2.png'), - clouds1X = 0, - clouds2X = 0, - animationTimer = nil, - setup = function(self) - self.animationTimer = playdate.timer.new(500, function() self:nextFrame() end) - self.animationTimer.repeats = true - end, - nextFrame = function(self) - self.clouds1X = self.clouds1X + 0.24 - self.clouds2X = self.clouds2X + 0.51 - if self.clouds1X > 800 then - self.clouds1X = 0 - end - if self.clouds2X > 800 then - self.clouds2X = 0 - end - self.bgImageIndex = self.bgImageIndex + 1 - if self.bgImageIndex > self.bgImageTable:getLength() then - self.bgImageIndex = 1 - end - end, - draw = function(self) - self.skyImage:drawIgnoringOffset(0, 0) - self.clouds1Image:drawIgnoringOffset(math.floor(self.clouds1X) - 800, 0) - self.clouds1Image:drawIgnoringOffset(math.floor(self.clouds1X), 0) - self.clouds2Image:drawIgnoringOffset(math.floor(self.clouds2X) - 800, 0) - self.clouds2Image:drawIgnoringOffset(math.floor(self.clouds2X), 0) - local bgImage = self.bgImageTable:getImage(self.bgImageIndex) - bgImage:drawIgnoringOffset(0, 0) - end - } - - s:setup() - return s - end -} - -return scene \ No newline at end of file +local scene = { + create = function() + local s = { + bgImageTable = Gfx.imagetable.new('images/bg.gif'), + bgImageIndex = 1, + skyImage = Gfx.image.new('images/sky.png'), + clouds1Image = Gfx.image.new('images/clouds1.png'), + clouds2Image = Gfx.image.new('images/clouds2.png'), + clouds1X = 0, + clouds2X = 0, + animationTimer = nil, + setup = function(self) + self.animationTimer = playdate.timer.new(500, function() self:nextFrame() end) + self.animationTimer.repeats = true + end, + nextFrame = function(self) + self.clouds1X = self.clouds1X + 0.24 + self.clouds2X = self.clouds2X + 0.51 + if self.clouds1X > 800 then + self.clouds1X = 0 + end + if self.clouds2X > 800 then + self.clouds2X = 0 + end + self.bgImageIndex = self.bgImageIndex + 1 + if self.bgImageIndex > self.bgImageTable:getLength() then + self.bgImageIndex = 1 + end + end, + draw = function(self) + self.skyImage:drawIgnoringOffset(0, 0) + self.clouds1Image:drawIgnoringOffset(math.floor(self.clouds1X) - 800, 0) + self.clouds1Image:drawIgnoringOffset(math.floor(self.clouds1X), 0) + self.clouds2Image:drawIgnoringOffset(math.floor(self.clouds2X) - 800, 0) + self.clouds2Image:drawIgnoringOffset(math.floor(self.clouds2X), 0) + local bgImage = self.bgImageTable:getImage(self.bgImageIndex) + bgImage:drawIgnoringOffset(0, 0) + end + } + + s:setup() + return s + end +} + +return scene diff --git a/stage.lua b/stage.lua index 6f30e4a..a9df376 100644 --- a/stage.lua +++ b/stage.lua @@ -1,732 +1,732 @@ -local stage = { - maxLevel = 10, - levelStrings = { "01", "02", "03", "04", "05", "06", "07", "08", "09", "10" }, - speeds = { 1000, 793, 618, 473, 355, 262, 190, 135, 94, 64 }, - - create = function(startMusic, nextSong, stopMusic) - local s = { - startMusic = startMusic, - nextSong = nextSong, - stopMusic = stopMusic, - - width = 10, - height = 22, - visibleHeight = 20, - - tetromino = nil, - enablePreview = true, - preview = nil, - enableHold = true, - hold = nil, - enableGhost = true, - ghost = nil, - - tiles = {}, - bag = {}, - - highScore = 0, - score = 0, - scoreDisplay = 0, - combo = -1, - linesCleared = 0, - fourLinesCleared = 0, - level = 1, - - speed = 1000, - lockDelay = 500, - lockTimer = nil, - tickTimer = nil, - holdDelay = 500, - isHardDropping = false, - isWaitingForHoldLock = false, - lastRowCleared = 1, - - isGameOver = false, - fillSpeed = 1, - fillTimer = nil, - fillRow = 22, - fillCol = 1, - - mode = "dynamic", - - levelLabelWidth = Gfx.getTextSize("LEVEL"), - holdLabelWidth = Gfx.getTextSize("HOLD"), - - lineClearAnimation = Gfx.imagetable.new('images/clear.gif'), - lineClearAnimationTimer = nil, - lineClearIndex = 1, - - bigLineClearAnimation = Gfx.imagetable.new('images/clearbig.gif'), - bigLineClearAnimationTimer = nil, - bigLineClearIndex = 1, - - highScoreAnimation = Gfx.imagetable.new('images/highscore.gif'), - highScoreAnimationTimer = nil, - highScoreIndex = 1, - hasReachedHighScore = false, - - soundEffects = { - shift = playdate.sound.sampleplayer.new("sounds/shift"), - rotate = playdate.sound.sampleplayer.new("sounds/rotate"), - hold = playdate.sound.sampleplayer.new("sounds/hold"), - softDrop = playdate.sound.sampleplayer.new("sounds/softDrop"), - hardDrop = playdate.sound.sampleplayer.new("sounds/hardDrop"), - collide = playdate.sound.sampleplayer.new("sounds/collide"), - land = playdate.sound.sampleplayer.new("sounds/land"), - lock = playdate.sound.sampleplayer.new("sounds/lock"), - lineClear1 = playdate.sound.sampleplayer.new("sounds/lineClear1"), - lineClear2 = playdate.sound.sampleplayer.new("sounds/lineClear2"), - lineClear3 = playdate.sound.sampleplayer.new("sounds/lineClear3"), - lineClear4 = playdate.sound.sampleplayer.new("sounds/lineClear4"), - gameOver = playdate.sound.sampleplayer.new("sounds/gameOver") - }, - - setup = function(self) - self.isGameOver = false - - self.score = 0 - self.scoreDisplay = 0 - self.combo = -1 - self.linesCleared = 0 - self.fourLinesCleared = 0 - self.hasReachedHighScore = false - - self.tetromino = nil - self.hold = nil - self.preview = nil - self.ghost = nil - - if self.mode ~= "chill" then - self:setLevel(1, true) - end - - self.tiles = {} - for row = 1, self.height do - self.tiles[row] = {} - for col = 1, self.width do - self.tiles[row][col] = 0 - end - end - - self:resetBag() - - self:startMusic() - - self.tickTimer = playdate.timer.new(self.speed, function() self:tick() end) - self.tickTimer.repeats = true - end, - - gameOver = function(self) - self.isGameOver = true - self.soundEffects.gameOver:play() - self:stopMusic() - self.tickTimer:remove() - self.tetromino = nil - - self.fillRow = 22 - self.fillCol = 1 - self.fillTimer = playdate.timer.new(self.fillSpeed, function() self:fill() end) - self.fillTimer.repeats = true - end, - - setMode = function(self, mode) - if self.mode ~= mode then - self.mode = mode - ModeOption:setValue(mode) - end - end, - - setLevel = function(self, level, setManually) - if level ~= self.level then - if self.mode == "chill" and not setManually then - level = self.level - end - - if level > Stage.maxLevel then - level = Stage.maxLevel - end - - self.level = level - - self.speed = Stage.speeds[level] - if self.tickTimer then - self.tickTimer.duration = self.speed - end - - if not setManually then - LevelOption:setValue(Stage.levelStrings[level]) - end - end - end, - - resetBag = function(self) - self.bag = {} - for i = 1, #Tetromino.types do - table.insert(self.bag, Tetromino.types[i]) - end - for i = #self.bag, 2, -1 do - local j = math.random(i) - self.bag[i], self.bag[j] = self.bag[j], self.bag[i] - end - end, - - chooseFromBag = function(self) - local type = self.bag[1] - table.remove(self.bag, 1) - if #self.bag == 0 then - self:resetBag() - end - return type - end, - - spawnTetromino = function(self) - local type = self:chooseFromBag() - local x = 4 - local y = 2 - if type == "I" then - y = 1 - end - - self.tetromino = Tetromino.create(type, x, y) - if self.tetromino:checkCollision(self, 0, 0) then - self:gameOver() - else - if self.enableGhost then - self.ghost = Tetromino.create(type, x, y) - self.ghost.isGhost = true - end - - if self.enablePreview then - self.preview = Tetromino.create(self.bag[1], 0, 0) - end - - self:tick() - end - end, - - shiftLeft = function(self) - if self.tetromino then - local success = self.tetromino:moveLeft(self) - if success then - self.soundEffects.shift:play() - else - self.soundEffects.collide:play() - end - end - end, - - shiftRight = function(self) - if self.tetromino then - local success = self.tetromino:moveRight(self) - if success then - self.soundEffects.shift:play() - else - self.soundEffects.collide:play() - end - end - end, - - rotateClockwise = function(self) - if self.tetromino then - local success = self.tetromino:rotateClockwise(self) - if success then - self.soundEffects.rotate:play() - else - self.soundEffects.collide:play() - end - end - end, - - rotateCounterClockwise = function(self) - if self.tetromino then - local success = self.tetromino:rotateCounterClockwise(self) - if success then - self.soundEffects.rotate:play() - else - self.soundEffects.collide:play() - end - end - end, - - startSoftDrop = function(self) - if self.tetromino then - self.soundEffects.softDrop:play(0) - self.tickTimer.duration = math.floor(self.speed / 20) - end - end, - - stopSoftDrop = function(self) - if self.tetromino then - self.soundEffects.softDrop:stop() - self.tickTimer.duration = self.speed - end - end, - - hardDrop = function(self) - if self.tetromino then - self.isHardDropping = true - self:tick() - end - end, - - switchHold = function(self) - if self.enableHold and not self.isWaitingForHoldLock and self.tetromino ~= nil then - self.isHardDropping = false - self.soundEffects.hold:play() - if self.hold then - table.insert(self.bag, 1, self.hold.type) - self.isWaitingForHoldLock = true - end - self.hold = self.tetromino - self.hold.rotationIndex = 1 - self:spawnTetromino() - end - end, - - lock = function(self) - if self.lockTimer then - self.lockTimer:remove() - self.lockTimer = nil - end - - if self.tetromino and self.tetromino:checkCollision(self, 0, 1) then - self:tetrominoToTiles() - self.tetromino = nil - local lineCount = self:checkForLines() - self:calculateScore(lineCount) - - if self:checkForLockOut() then - self:gameOver() - else - self:checkForSlowDown() - if lineCount == 0 then - if self.isHardDropping then - self.soundEffects.hardDrop:play() - else - self.soundEffects.lock:play() - end - elseif 1 <= lineCount and lineCount <= 4 then - self.soundEffects["lineClear" .. lineCount]:play() - end - end - end - - self.isWaitingForHoldLock = false - self.isHardDropping = false - end, - - countAirBubbles = function(self) - local airBubbles = 0 - local lastAirBubbleRow = 0 - local tilesLookedAt = {} - for row = 1, self.height do - local rowTiles = {} - for col = 1, self.width do - table.insert(rowTiles, false) - end - table.insert(tilesLookedAt, rowTiles) - end - - local lookAtTile - lookAtTile = function(row, col) - if self.tiles[row][col] == 0 and not tilesLookedAt[row][col] then - tilesLookedAt[row][col] = true - if row - 1 >= 1 then - lookAtTile(row - 1, col) - end - if row + 1 <= self.height then - lookAtTile(row + 1, col) - end - if col - 1 >= 1 then - lookAtTile(row, col - 1) - end - if col + 1 <= self.width then - lookAtTile(row, col + 1) - end - end - end - - for row = 1, self.height do - for col = 1, self.width do - if self.tiles[row][col] == 0 and not tilesLookedAt[row][col] then - if row > lastAirBubbleRow then - airBubbles = airBubbles + 1 - lastAirBubbleRow = row - end - lookAtTile(row, col) - end - end - end - - return airBubbles - 1 - end, - - checkForSlowDown = function(self) - if self.mode == "dynamic" and self.level > 1 then - local airBubbles = self:countAirBubbles() - local newMaxLevel = Stage.maxLevel - (airBubbles - 1) - if self.level > newMaxLevel then - self:setLevel(self.level - 1) - end - end - end, - - checkForLockOut = function(self) - local tilesAboveVisibleStage = false - for row = 1, 2 do - for col = 1, self.width do - if self.tiles[row][col] > 0 then - tilesAboveVisibleStage = true - break - end - end - end - return tilesAboveVisibleStage - end, - - checkForLines = function(self) - local lineCount = 0 - self.lastRowCleared = 1 - for row = 3, self.height do - local isLine = true - for col = 1, self.width do - if self.tiles[row][col] == 0 then - isLine = false - end - end - if isLine then - self:clearLine(row) - lineCount = lineCount + 1 - if row > self.lastRowCleared then - self.lastRowCleared = row - end - end - end - return lineCount - end, - - clearLine = function(self, y) - for row = y, 2, -1 do - for col = 1, self.width do - self.tiles[row][col] = self.tiles[row - 1][col] - end - end - end, - - tetrominoToTiles = function(self) - local pattern = Tetromino.pattern[self.tetromino.type] - local rotation = pattern[self.tetromino.rotationIndex] - for row = 1, #rotation do - for col = 1, #rotation[row] do - if rotation[row][col] > 0 then - local x = self.tetromino.x + col - 1 - local y = self.tetromino.y + row - 1 - self.tiles[y][x] = rotation[row][col] - end - end - end - end, - - calculateScore = function(self, lineCount) - local score = 0 - - if lineCount == 1 then - score = 100 * self.level - elseif lineCount == 2 then - score = 300 * self.level - elseif lineCount == 3 then - score = 500 * self.level - elseif lineCount >= 4 then - score = 800 * self.level - end - - if lineCount >= 1 then - self.combo = self.combo + 1 - score = score + (50 * self.combo * self.level) - else - self.combo = -1 - end - - self.linesCleared = self.linesCleared + lineCount - if self.linesCleared >= 10 then - self.linesCleared = 0 - self:setLevel(self.level + 1) - end - - if lineCount >= 4 then - self.fourLinesCleared = self.fourLinesCleared + 1 - if self.fourLinesCleared >= 2 then - self.fourLinesCleared = 0 - self:nextSong() - end - end - - if lineCount >= 1 then - self.lineClearIndex = 1 - self.lineClearAnimationTimer = playdate.timer.new(25, function() self:nextLineClearFrame() end) - self.lineClearAnimationTimer.repeats = true - end - if lineCount >= 4 then - self.bigLineClearIndex = 1 - self.bigLineClearAnimationTimer = playdate.timer.new(30, function() self:nextBigLineClearFrame() end) - self.bigLineClearAnimationTimer.repeats = true - end - - self.score = self.score + score - - if self.score > self.highScore then - if self.highScoreAnimationTimer == nil and not self.hasReachedHighScore then - self.highScoreIndex = 1 - self.highScoreAnimationTimer = playdate.timer.new(30, function() self:nextHighScoreFrame() end) - self.highScoreAnimationTimer.repeats = true - end - self.highScore = self.score - self.hasReachedHighScore = true - end - end, - - tick = function(self) - if self.tetromino then - - local success = false - if self.isHardDropping then - while self.tetromino:moveDown(self) do - -- nothing, we're hard-dropping until we hit the bottom - end - else - success = self.tetromino:moveDown(self) - end - - if success then - if self.lockTimer then - self.lockTimer:remove() - self.lockTimer = nil - end - elseif not self.lockTimer then - local lockDelay = self.lockDelay - if self.isHardDropping then - lockDelay = 1 - else - self.soundEffects.land:play() - end - self.lockTimer = playdate.timer.new(lockDelay, function() self:lock() end) - end - - else - self:spawnTetromino() - end - end, - - fill = function(self) - self.tiles[self.fillRow][self.fillCol] = math.random(1, 7) - - if self.fillRow % 2 == 0 then - self.fillCol = self.fillCol + 1 - else - self.fillCol = self.fillCol - 1 - end - - if self.fillCol < 1 or self.fillCol > self.width then - self.fillRow = self.fillRow - 1 - if self.fillRow % 2 == 0 then - self.fillCol = 1 - else - self.fillCol = self.width - end - if self.fillRow > 2 then - self.soundEffects.collide:play() - end - end - - if self.fillRow <= 2 then - self.soundEffects.land:play() - self.fillTimer:remove() - self.fillTimer = nil - end - end, - - drawTile = function(self, tx, ty, blockIndex) - local x = tx * Tetromino.minoSize - local y = ty * Tetromino.minoSize - Tetromino.images[blockIndex]:draw(x, y) - end, - - draw = function(self) - local displayWidth = self.width * Tetromino.minoSize - local displayHeight = self.visibleHeight * Tetromino.minoSize - - if self.score > self.scoreDisplay then - if self.score - self.scoreDisplay >= 1000 then - self.scoreDisplay = math.min(self.score, self.scoreDisplay + 100) - elseif self.score - self.scoreDisplay >= 100 then - self.scoreDisplay = math.min(self.score, self.scoreDisplay + 20) - else - self.scoreDisplay = math.min(self.score, self.scoreDisplay + 10) - end - end - Gfx.drawText("SCORE", displayWidth + 10, displayHeight - 26) - Gfx.drawText(self.scoreDisplay, displayWidth + 10, displayHeight - 10) - - Gfx.drawText("LEVEL", -self.levelLabelWidth - 10, displayHeight - 26) - self.levelTextWidth = Gfx.getTextSize(self.level) - Gfx.drawText(self.level, -self.levelTextWidth - 10, displayHeight - 10) - - if self.enablePreview then - playdate.graphics.setImageDrawMode(playdate.graphics.kDrawModeFillWhite) - Gfx.drawText("NEXT", displayWidth + 9, -1) - Gfx.drawText("NEXT", displayWidth + 9, 0) - Gfx.drawText("NEXT", displayWidth + 9, 1) - Gfx.drawText("NEXT", displayWidth + 11, -1) - Gfx.drawText("NEXT", displayWidth + 11, 0) - Gfx.drawText("NEXT", displayWidth + 11, 1) - Gfx.drawText("NEXT", displayWidth + 10, -1) - Gfx.drawText("NEXT", displayWidth + 10, 1) - playdate.graphics.setImageDrawMode(playdate.graphics.kDrawModeCopy) - Gfx.drawText("NEXT", displayWidth + 10, 0) - end - - if self.enableHold then - playdate.graphics.setImageDrawMode(playdate.graphics.kDrawModeFillWhite) - Gfx.drawText("HOLD", -self.holdLabelWidth - 9, -1) - Gfx.drawText("HOLD", -self.holdLabelWidth - 9, 0) - Gfx.drawText("HOLD", -self.holdLabelWidth - 9, 1) - Gfx.drawText("HOLD", -self.holdLabelWidth - 11, -1) - Gfx.drawText("HOLD", -self.holdLabelWidth - 11, 0) - Gfx.drawText("HOLD", -self.holdLabelWidth - 11, 1) - Gfx.drawText("HOLD", -self.holdLabelWidth - 10, -1) - Gfx.drawText("HOLD", -self.holdLabelWidth - 10, 1) - playdate.graphics.setImageDrawMode(playdate.graphics.kDrawModeCopy) - Gfx.drawText("HOLD", -self.holdLabelWidth - 10, 0) - end - - if self.enablePreview and self.preview then - self.preview.x = self.width + 2 - self.preview.y = 5 - self.preview:draw(true) - end - - if self.enableHold and self.hold then - self.hold.x = -Tetromino.width[self.hold.type] - self.hold.y = 5 - self.hold:draw(true) - end - - Gfx.setColor(Gfx.kColorWhite) - Gfx.fillRect(-3, -3, displayWidth + 5, displayHeight + 5) - Gfx.setColor(Gfx.kColorBlack) - Gfx.drawRect(-2, -2, displayWidth + 3, displayHeight + 3) - Gfx.setClipRect(0, 0, displayWidth, displayHeight) - - if self.tetromino then - if self.ghost and self.enableGhost then - self.ghost.x = self.tetromino.x - self.ghost.y = self.tetromino.y - self.ghost.rotationIndex = self.tetromino.rotationIndex - while self.ghost:moveDown(self) do end - self.ghost:draw() - end - - self.tetromino:draw() - end - - for row = 1, self.height do - for col = 1, self.width do - if self.tiles[row][col] > 0 then - local x = col - 1 - local y = row - 3 - self:drawTile(x, y, self.tiles[row][col]) - end - end - end - - playdate.graphics.clearClipRect() - - if self.lineClearAnimationTimer ~= nil then - local lineClearImage = self.lineClearAnimation:getImage(self.lineClearIndex) - local lineClearY = (self.lastRowCleared - 3) * Tetromino.minoSize - 5 - lineClearImage:draw(-13, lineClearY) - end - - if self.bigLineClearAnimationTimer ~= nil then - local bigLineClearImage = self.bigLineClearAnimation:getImage(self.bigLineClearIndex) - local bigLineClearY = (self.lastRowCleared - 3) * Tetromino.minoSize - 10 - bigLineClearImage:draw(-13, bigLineClearY) - end - - if self.highScoreAnimationTimer ~= nil then - local highScoreImage = self.highScoreAnimation:getImage(self.highScoreIndex) - local scoreWidth = playdate.graphics.getTextSize(self.score) - highScoreImage:draw(displayWidth + scoreWidth + 12, displayHeight - 17) - elseif self.hasReachedHighScore then - local highScoreImage = self.highScoreAnimation:getImage(4) - local scoreWidth = playdate.graphics.getTextSize(self.score) - highScoreImage:draw(displayWidth + scoreWidth + 12, displayHeight - 17) - end - - if self.isGameOver then - local highScoreText = "HIGH SCORE\n" .. self.highScore - local highScoreW, highScoreH = playdate.graphics.getTextSize(highScoreText, nil, 4) - local highScoreX = displayWidth / 2 - math.floor(highScoreW / 2) - local highScoreY = displayHeight / 2 - math.floor(highScoreH / 2) - playdate.graphics.setColor(playdate.graphics.kColorWhite) - playdate.graphics.fillRoundRect(highScoreX - 4, highScoreY - 4, highScoreW + 8, highScoreH + 8, 4) - Gfx.drawTextAligned(highScoreText, displayWidth / 2, highScoreY, kTextAlignment.center, 4) - end - - end, - - nextLineClearFrame = function(self) - self.lineClearIndex = self.lineClearIndex + 1 - if self.lineClearIndex >= self.lineClearAnimation:getLength() then - self.lineClearAnimationTimer:remove() - self.lineClearAnimationTimer = nil - end - end, - - nextBigLineClearFrame = function(self) - self.bigLineClearIndex = self.bigLineClearIndex + 1 - if self.bigLineClearIndex >= self.bigLineClearAnimation:getLength() then - self.bigLineClearAnimationTimer:remove() - self.bigLineClearAnimationTimer = nil - end - end, - - nextHighScoreFrame = function(self) - self.highScoreIndex = self.highScoreIndex + 1 - if self.highScoreIndex >= self.highScoreAnimation:getLength() then - self.highScoreAnimationTimer:remove() - self.highScoreAnimationTimer = nil - end - end, - - saveData = function(self) - playdate.datastore.write({ - highScore = self.highScore, - mode = self.mode, - level = self.level - }) - end, - - loadData = function(self) - local data = playdate.datastore.read() - if data ~= nil then - if data.highScore ~= nil then - self.highScore = data.highScore - end - if data.mode ~= nil then - self:setMode(data.mode) - end - if data.level ~= nil and data.mode == "chill" then - self:setLevel(data.level, true) - LevelOption:setValue(Stage.levelStrings[data.level]) - end - end - end, - } - - s:setup() - return s - end -} - -return stage \ No newline at end of file +local stage = { + maxLevel = 10, + levelStrings = { "01", "02", "03", "04", "05", "06", "07", "08", "09", "10" }, + speeds = { 1000, 793, 618, 473, 355, 262, 190, 135, 94, 64 }, + + create = function(startMusic, nextSong, stopMusic) + local s = { + startMusic = startMusic, + nextSong = nextSong, + stopMusic = stopMusic, + + width = 10, + height = 22, + visibleHeight = 20, + + tetromino = nil, + enablePreview = true, + preview = nil, + enableHold = true, + hold = nil, + enableGhost = true, + ghost = nil, + + tiles = {}, + bag = {}, + + highScore = 0, + score = 0, + scoreDisplay = 0, + combo = -1, + linesCleared = 0, + fourLinesCleared = 0, + level = 1, + + speed = 1000, + lockDelay = 500, + lockTimer = nil, + tickTimer = nil, + holdDelay = 500, + isHardDropping = false, + isWaitingForHoldLock = false, + lastRowCleared = 1, + + isGameOver = false, + fillSpeed = 1, + fillTimer = nil, + fillRow = 22, + fillCol = 1, + + mode = "dynamic", + + levelLabelWidth = Gfx.getTextSize("LEVEL"), + holdLabelWidth = Gfx.getTextSize("HOLD"), + + lineClearAnimation = Gfx.imagetable.new('images/clear.gif'), + lineClearAnimationTimer = nil, + lineClearIndex = 1, + + bigLineClearAnimation = Gfx.imagetable.new('images/clearbig.gif'), + bigLineClearAnimationTimer = nil, + bigLineClearIndex = 1, + + highScoreAnimation = Gfx.imagetable.new('images/highscore.gif'), + highScoreAnimationTimer = nil, + highScoreIndex = 1, + hasReachedHighScore = false, + + soundEffects = { + shift = playdate.sound.sampleplayer.new("sounds/shift"), + rotate = playdate.sound.sampleplayer.new("sounds/rotate"), + hold = playdate.sound.sampleplayer.new("sounds/hold"), + softDrop = playdate.sound.sampleplayer.new("sounds/softDrop"), + hardDrop = playdate.sound.sampleplayer.new("sounds/hardDrop"), + collide = playdate.sound.sampleplayer.new("sounds/collide"), + land = playdate.sound.sampleplayer.new("sounds/land"), + lock = playdate.sound.sampleplayer.new("sounds/lock"), + lineClear1 = playdate.sound.sampleplayer.new("sounds/lineClear1"), + lineClear2 = playdate.sound.sampleplayer.new("sounds/lineClear2"), + lineClear3 = playdate.sound.sampleplayer.new("sounds/lineClear3"), + lineClear4 = playdate.sound.sampleplayer.new("sounds/lineClear4"), + gameOver = playdate.sound.sampleplayer.new("sounds/gameOver") + }, + + setup = function(self) + self.isGameOver = false + + self.score = 0 + self.scoreDisplay = 0 + self.combo = -1 + self.linesCleared = 0 + self.fourLinesCleared = 0 + self.hasReachedHighScore = false + + self.tetromino = nil + self.hold = nil + self.preview = nil + self.ghost = nil + + if self.mode ~= "chill" then + self:setLevel(1, true) + end + + self.tiles = {} + for row = 1, self.height do + self.tiles[row] = {} + for col = 1, self.width do + self.tiles[row][col] = 0 + end + end + + self:resetBag() + + self:startMusic() + + self.tickTimer = playdate.timer.new(self.speed, function() self:tick() end) + self.tickTimer.repeats = true + end, + + gameOver = function(self) + self.isGameOver = true + self.soundEffects.gameOver:play() + self:stopMusic() + self.tickTimer:remove() + self.tetromino = nil + + self.fillRow = 22 + self.fillCol = 1 + self.fillTimer = playdate.timer.new(self.fillSpeed, function() self:fill() end) + self.fillTimer.repeats = true + end, + + setMode = function(self, mode) + if self.mode ~= mode then + self.mode = mode + ModeOption:setValue(mode) + end + end, + + setLevel = function(self, level, setManually) + if level ~= self.level then + if self.mode == "chill" and not setManually then + level = self.level + end + + if level > Stage.maxLevel then + level = Stage.maxLevel + end + + self.level = level + + self.speed = Stage.speeds[level] + if self.tickTimer then + self.tickTimer.duration = self.speed + end + + if not setManually then + LevelOption:setValue(Stage.levelStrings[level]) + end + end + end, + + resetBag = function(self) + self.bag = {} + for i = 1, #Tetromino.types do + table.insert(self.bag, Tetromino.types[i]) + end + for i = #self.bag, 2, -1 do + local j = math.random(i) + self.bag[i], self.bag[j] = self.bag[j], self.bag[i] + end + end, + + chooseFromBag = function(self) + local type = self.bag[1] + table.remove(self.bag, 1) + if #self.bag == 0 then + self:resetBag() + end + return type + end, + + spawnTetromino = function(self) + local type = self:chooseFromBag() + local x = 4 + local y = 2 + if type == "I" then + y = 1 + end + + self.tetromino = Tetromino.create(type, x, y) + if self.tetromino:checkCollision(self, 0, 0) then + self:gameOver() + else + if self.enableGhost then + self.ghost = Tetromino.create(type, x, y) + self.ghost.isGhost = true + end + + if self.enablePreview then + self.preview = Tetromino.create(self.bag[1], 0, 0) + end + + self:tick() + end + end, + + shiftLeft = function(self) + if self.tetromino then + local success = self.tetromino:moveLeft(self) + if success then + self.soundEffects.shift:play() + else + self.soundEffects.collide:play() + end + end + end, + + shiftRight = function(self) + if self.tetromino then + local success = self.tetromino:moveRight(self) + if success then + self.soundEffects.shift:play() + else + self.soundEffects.collide:play() + end + end + end, + + rotateClockwise = function(self) + if self.tetromino then + local success = self.tetromino:rotateClockwise(self) + if success then + self.soundEffects.rotate:play() + else + self.soundEffects.collide:play() + end + end + end, + + rotateCounterClockwise = function(self) + if self.tetromino then + local success = self.tetromino:rotateCounterClockwise(self) + if success then + self.soundEffects.rotate:play() + else + self.soundEffects.collide:play() + end + end + end, + + startSoftDrop = function(self) + if self.tetromino then + self.soundEffects.softDrop:play(0) + self.tickTimer.duration = math.floor(self.speed / 20) + end + end, + + stopSoftDrop = function(self) + if self.tetromino then + self.soundEffects.softDrop:stop() + self.tickTimer.duration = self.speed + end + end, + + hardDrop = function(self) + if self.tetromino then + self.isHardDropping = true + self:tick() + end + end, + + switchHold = function(self) + if self.enableHold and not self.isWaitingForHoldLock and self.tetromino ~= nil then + self.isHardDropping = false + self.soundEffects.hold:play() + if self.hold then + table.insert(self.bag, 1, self.hold.type) + self.isWaitingForHoldLock = true + end + self.hold = self.tetromino + self.hold.rotationIndex = 1 + self:spawnTetromino() + end + end, + + lock = function(self) + if self.lockTimer then + self.lockTimer:remove() + self.lockTimer = nil + end + + if self.tetromino and self.tetromino:checkCollision(self, 0, 1) then + self:tetrominoToTiles() + self.tetromino = nil + local lineCount = self:checkForLines() + self:calculateScore(lineCount) + + if self:checkForLockOut() then + self:gameOver() + else + self:checkForSlowDown() + if lineCount == 0 then + if self.isHardDropping then + self.soundEffects.hardDrop:play() + else + self.soundEffects.lock:play() + end + elseif 1 <= lineCount and lineCount <= 4 then + self.soundEffects["lineClear" .. lineCount]:play() + end + end + end + + self.isWaitingForHoldLock = false + self.isHardDropping = false + end, + + countAirBubbles = function(self) + local airBubbles = 0 + local lastAirBubbleRow = 0 + local tilesLookedAt = {} + for row = 1, self.height do + local rowTiles = {} + for col = 1, self.width do + table.insert(rowTiles, false) + end + table.insert(tilesLookedAt, rowTiles) + end + + local lookAtTile + lookAtTile = function(row, col) + if self.tiles[row][col] == 0 and not tilesLookedAt[row][col] then + tilesLookedAt[row][col] = true + if row - 1 >= 1 then + lookAtTile(row - 1, col) + end + if row + 1 <= self.height then + lookAtTile(row + 1, col) + end + if col - 1 >= 1 then + lookAtTile(row, col - 1) + end + if col + 1 <= self.width then + lookAtTile(row, col + 1) + end + end + end + + for row = 1, self.height do + for col = 1, self.width do + if self.tiles[row][col] == 0 and not tilesLookedAt[row][col] then + if row > lastAirBubbleRow then + airBubbles = airBubbles + 1 + lastAirBubbleRow = row + end + lookAtTile(row, col) + end + end + end + + return airBubbles - 1 + end, + + checkForSlowDown = function(self) + if self.mode == "dynamic" and self.level > 1 then + local airBubbles = self:countAirBubbles() + local newMaxLevel = Stage.maxLevel - (airBubbles - 1) + if self.level > newMaxLevel then + self:setLevel(self.level - 1) + end + end + end, + + checkForLockOut = function(self) + local tilesAboveVisibleStage = false + for row = 1, 2 do + for col = 1, self.width do + if self.tiles[row][col] > 0 then + tilesAboveVisibleStage = true + break + end + end + end + return tilesAboveVisibleStage + end, + + checkForLines = function(self) + local lineCount = 0 + self.lastRowCleared = 1 + for row = 3, self.height do + local isLine = true + for col = 1, self.width do + if self.tiles[row][col] == 0 then + isLine = false + end + end + if isLine then + self:clearLine(row) + lineCount = lineCount + 1 + if row > self.lastRowCleared then + self.lastRowCleared = row + end + end + end + return lineCount + end, + + clearLine = function(self, y) + for row = y, 2, -1 do + for col = 1, self.width do + self.tiles[row][col] = self.tiles[row - 1][col] + end + end + end, + + tetrominoToTiles = function(self) + local pattern = Tetromino.pattern[self.tetromino.type] + local rotation = pattern[self.tetromino.rotationIndex] + for row = 1, #rotation do + for col = 1, #rotation[row] do + if rotation[row][col] > 0 then + local x = self.tetromino.x + col - 1 + local y = self.tetromino.y + row - 1 + self.tiles[y][x] = rotation[row][col] + end + end + end + end, + + calculateScore = function(self, lineCount) + local score = 0 + + if lineCount == 1 then + score = 100 * self.level + elseif lineCount == 2 then + score = 300 * self.level + elseif lineCount == 3 then + score = 500 * self.level + elseif lineCount >= 4 then + score = 800 * self.level + end + + if lineCount >= 1 then + self.combo = self.combo + 1 + score = score + (50 * self.combo * self.level) + else + self.combo = -1 + end + + self.linesCleared = self.linesCleared + lineCount + if self.linesCleared >= 10 then + self.linesCleared = 0 + self:setLevel(self.level + 1) + end + + if lineCount >= 4 then + self.fourLinesCleared = self.fourLinesCleared + 1 + if self.fourLinesCleared >= 2 then + self.fourLinesCleared = 0 + self:nextSong() + end + end + + if lineCount >= 1 then + self.lineClearIndex = 1 + self.lineClearAnimationTimer = playdate.timer.new(25, function() self:nextLineClearFrame() end) + self.lineClearAnimationTimer.repeats = true + end + if lineCount >= 4 then + self.bigLineClearIndex = 1 + self.bigLineClearAnimationTimer = playdate.timer.new(30, function() self:nextBigLineClearFrame() end) + self.bigLineClearAnimationTimer.repeats = true + end + + self.score = self.score + score + + if self.score > self.highScore then + if self.highScoreAnimationTimer == nil and not self.hasReachedHighScore then + self.highScoreIndex = 1 + self.highScoreAnimationTimer = playdate.timer.new(30, function() self:nextHighScoreFrame() end) + self.highScoreAnimationTimer.repeats = true + end + self.highScore = self.score + self.hasReachedHighScore = true + end + end, + + tick = function(self) + if self.tetromino then + + local success = false + if self.isHardDropping then + while self.tetromino:moveDown(self) do + -- nothing, we're hard-dropping until we hit the bottom + end + else + success = self.tetromino:moveDown(self) + end + + if success then + if self.lockTimer then + self.lockTimer:remove() + self.lockTimer = nil + end + elseif not self.lockTimer then + local lockDelay = self.lockDelay + if self.isHardDropping then + lockDelay = 1 + else + self.soundEffects.land:play() + end + self.lockTimer = playdate.timer.new(lockDelay, function() self:lock() end) + end + + else + self:spawnTetromino() + end + end, + + fill = function(self) + self.tiles[self.fillRow][self.fillCol] = math.random(1, 7) + + if self.fillRow % 2 == 0 then + self.fillCol = self.fillCol + 1 + else + self.fillCol = self.fillCol - 1 + end + + if self.fillCol < 1 or self.fillCol > self.width then + self.fillRow = self.fillRow - 1 + if self.fillRow % 2 == 0 then + self.fillCol = 1 + else + self.fillCol = self.width + end + if self.fillRow > 2 then + self.soundEffects.collide:play() + end + end + + if self.fillRow <= 2 then + self.soundEffects.land:play() + self.fillTimer:remove() + self.fillTimer = nil + end + end, + + drawTile = function(self, tx, ty, blockIndex) + local x = tx * Tetromino.minoSize + local y = ty * Tetromino.minoSize + Tetromino.images[blockIndex]:draw(x, y) + end, + + draw = function(self) + local displayWidth = self.width * Tetromino.minoSize + local displayHeight = self.visibleHeight * Tetromino.minoSize + + if self.score > self.scoreDisplay then + if self.score - self.scoreDisplay >= 1000 then + self.scoreDisplay = math.min(self.score, self.scoreDisplay + 100) + elseif self.score - self.scoreDisplay >= 100 then + self.scoreDisplay = math.min(self.score, self.scoreDisplay + 20) + else + self.scoreDisplay = math.min(self.score, self.scoreDisplay + 10) + end + end + Gfx.drawText("SCORE", displayWidth + 10, displayHeight - 26) + Gfx.drawText(self.scoreDisplay, displayWidth + 10, displayHeight - 10) + + Gfx.drawText("LEVEL", -self.levelLabelWidth - 10, displayHeight - 26) + self.levelTextWidth = Gfx.getTextSize(self.level) + Gfx.drawText(self.level, -self.levelTextWidth - 10, displayHeight - 10) + + if self.enablePreview then + playdate.graphics.setImageDrawMode(playdate.graphics.kDrawModeFillWhite) + Gfx.drawText("NEXT", displayWidth + 9, -1) + Gfx.drawText("NEXT", displayWidth + 9, 0) + Gfx.drawText("NEXT", displayWidth + 9, 1) + Gfx.drawText("NEXT", displayWidth + 11, -1) + Gfx.drawText("NEXT", displayWidth + 11, 0) + Gfx.drawText("NEXT", displayWidth + 11, 1) + Gfx.drawText("NEXT", displayWidth + 10, -1) + Gfx.drawText("NEXT", displayWidth + 10, 1) + playdate.graphics.setImageDrawMode(playdate.graphics.kDrawModeCopy) + Gfx.drawText("NEXT", displayWidth + 10, 0) + end + + if self.enableHold then + playdate.graphics.setImageDrawMode(playdate.graphics.kDrawModeFillWhite) + Gfx.drawText("HOLD", -self.holdLabelWidth - 9, -1) + Gfx.drawText("HOLD", -self.holdLabelWidth - 9, 0) + Gfx.drawText("HOLD", -self.holdLabelWidth - 9, 1) + Gfx.drawText("HOLD", -self.holdLabelWidth - 11, -1) + Gfx.drawText("HOLD", -self.holdLabelWidth - 11, 0) + Gfx.drawText("HOLD", -self.holdLabelWidth - 11, 1) + Gfx.drawText("HOLD", -self.holdLabelWidth - 10, -1) + Gfx.drawText("HOLD", -self.holdLabelWidth - 10, 1) + playdate.graphics.setImageDrawMode(playdate.graphics.kDrawModeCopy) + Gfx.drawText("HOLD", -self.holdLabelWidth - 10, 0) + end + + if self.enablePreview and self.preview then + self.preview.x = self.width + 2 + self.preview.y = 5 + self.preview:draw(true) + end + + if self.enableHold and self.hold then + self.hold.x = -Tetromino.width[self.hold.type] + self.hold.y = 5 + self.hold:draw(true) + end + + Gfx.setColor(Gfx.kColorWhite) + Gfx.fillRect(-3, -3, displayWidth + 5, displayHeight + 5) + Gfx.setColor(Gfx.kColorBlack) + Gfx.drawRect(-2, -2, displayWidth + 3, displayHeight + 3) + Gfx.setClipRect(0, 0, displayWidth, displayHeight) + + if self.tetromino then + if self.ghost and self.enableGhost then + self.ghost.x = self.tetromino.x + self.ghost.y = self.tetromino.y + self.ghost.rotationIndex = self.tetromino.rotationIndex + while self.ghost:moveDown(self) do end + self.ghost:draw() + end + + self.tetromino:draw() + end + + for row = 1, self.height do + for col = 1, self.width do + if self.tiles[row][col] > 0 then + local x = col - 1 + local y = row - 3 + self:drawTile(x, y, self.tiles[row][col]) + end + end + end + + playdate.graphics.clearClipRect() + + if self.lineClearAnimationTimer ~= nil then + local lineClearImage = self.lineClearAnimation:getImage(self.lineClearIndex) + local lineClearY = (self.lastRowCleared - 3) * Tetromino.minoSize - 5 + lineClearImage:draw(-13, lineClearY) + end + + if self.bigLineClearAnimationTimer ~= nil then + local bigLineClearImage = self.bigLineClearAnimation:getImage(self.bigLineClearIndex) + local bigLineClearY = (self.lastRowCleared - 3) * Tetromino.minoSize - 10 + bigLineClearImage:draw(-13, bigLineClearY) + end + + if self.highScoreAnimationTimer ~= nil then + local highScoreImage = self.highScoreAnimation:getImage(self.highScoreIndex) + local scoreWidth = playdate.graphics.getTextSize(self.score) + highScoreImage:draw(displayWidth + scoreWidth + 12, displayHeight - 17) + elseif self.hasReachedHighScore then + local highScoreImage = self.highScoreAnimation:getImage(4) + local scoreWidth = playdate.graphics.getTextSize(self.score) + highScoreImage:draw(displayWidth + scoreWidth + 12, displayHeight - 17) + end + + if self.isGameOver then + local highScoreText = "HIGH SCORE\n" .. self.highScore + local highScoreW, highScoreH = playdate.graphics.getTextSize(highScoreText, nil, 4) + local highScoreX = displayWidth / 2 - math.floor(highScoreW / 2) + local highScoreY = displayHeight / 2 - math.floor(highScoreH / 2) + playdate.graphics.setColor(playdate.graphics.kColorWhite) + playdate.graphics.fillRoundRect(highScoreX - 4, highScoreY - 4, highScoreW + 8, highScoreH + 8, 4) + Gfx.drawTextAligned(highScoreText, displayWidth / 2, highScoreY, kTextAlignment.center, 4) + end + + end, + + nextLineClearFrame = function(self) + self.lineClearIndex = self.lineClearIndex + 1 + if self.lineClearIndex >= self.lineClearAnimation:getLength() then + self.lineClearAnimationTimer:remove() + self.lineClearAnimationTimer = nil + end + end, + + nextBigLineClearFrame = function(self) + self.bigLineClearIndex = self.bigLineClearIndex + 1 + if self.bigLineClearIndex >= self.bigLineClearAnimation:getLength() then + self.bigLineClearAnimationTimer:remove() + self.bigLineClearAnimationTimer = nil + end + end, + + nextHighScoreFrame = function(self) + self.highScoreIndex = self.highScoreIndex + 1 + if self.highScoreIndex >= self.highScoreAnimation:getLength() then + self.highScoreAnimationTimer:remove() + self.highScoreAnimationTimer = nil + end + end, + + saveData = function(self) + playdate.datastore.write({ + highScore = self.highScore, + mode = self.mode, + level = self.level + }) + end, + + loadData = function(self) + local data = playdate.datastore.read() + if data ~= nil then + if data.highScore ~= nil then + self.highScore = data.highScore + end + if data.mode ~= nil then + self:setMode(data.mode) + end + if data.level ~= nil and data.mode == "chill" then + self:setLevel(data.level, true) + LevelOption:setValue(Stage.levelStrings[data.level]) + end + end + end, + } + + s:setup() + return s + end +} + +return stage diff --git a/tetromino.lua b/tetromino.lua index ca331ac..e1d776b 100644 --- a/tetromino.lua +++ b/tetromino.lua @@ -1,329 +1,329 @@ -local tetromino = { - minoSize = 10, - - types = { - "I", - "J", - "L", - "S", - "Z", - "T", - "O", - }, - - width = { - I = 4, - J = 3, - L = 3, - S = 3, - Z = 3, - T = 3, - O = 3, - }, - - pattern = { - I = { - { - { 0, 0, 0, 0 }, - { 1, 1, 1, 1 }, - { 0, 0, 0, 0 }, - { 0, 0, 0, 0 }, - }, - { - { 0, 0, 1, 0 }, - { 0, 0, 1, 0 }, - { 0, 0, 1, 0 }, - { 0, 0, 1, 0 }, - }, - { - { 0, 0, 0, 0 }, - { 0, 0, 0, 0 }, - { 1, 1, 1, 1 }, - { 0, 0, 0, 0 }, - }, - { - { 0, 1, 0, 0 }, - { 0, 1, 0, 0 }, - { 0, 1, 0, 0 }, - { 0, 1, 0, 0 }, - }, - }, - J = { - { - { 2, 0, 0 }, - { 2, 2, 2 }, - { 0, 0, 0 }, - }, - { - { 0, 2, 2 }, - { 0, 2, 0 }, - { 0, 2, 0 }, - }, - { - { 0, 0, 0 }, - { 2, 2, 2 }, - { 0, 0, 2 }, - }, - { - { 0, 2, 0 }, - { 0, 2, 0 }, - { 2, 2, 0 }, - }, - }, - L = { - { - { 0, 0, 3 }, - { 3, 3, 3 }, - { 0, 0, 0 }, - }, - { - { 0, 3, 0 }, - { 0, 3, 0 }, - { 0, 3, 3 }, - }, - { - { 0, 0, 0 }, - { 3, 3, 3 }, - { 3, 0, 0 }, - }, - { - { 3, 3, 0 }, - { 0, 3, 0 }, - { 0, 3, 0 }, - }, - }, - S = { - { - { 0, 4, 4 }, - { 4, 4, 0 }, - { 0, 0, 0 }, - }, - { - { 0, 4, 0 }, - { 0, 4, 4 }, - { 0, 0, 4 }, - }, - { - { 0, 0, 0 }, - { 0, 4, 4 }, - { 4, 4, 0 }, - }, - { - { 4, 0, 0 }, - { 4, 4, 0 }, - { 0, 4, 0 }, - }, - }, - Z = { - { - { 5, 5, 0 }, - { 0, 5, 5 }, - { 0, 0, 0 }, - }, - { - { 0, 0, 5 }, - { 0, 5, 5 }, - { 0, 5, 0 }, - }, - { - { 0, 0, 0 }, - { 5, 5, 0 }, - { 0, 5, 5 }, - }, - { - { 0, 5, 0 }, - { 5, 5, 0 }, - { 5, 0, 0 }, - }, - }, - T = { - { - { 0, 6, 0 }, - { 6, 6, 6 }, - { 0, 0, 0 }, - }, - { - { 0, 6, 0 }, - { 0, 6, 6 }, - { 0, 6, 0 }, - }, - { - { 0, 0, 0 }, - { 6, 6, 6 }, - { 0, 6, 0 }, - }, - { - { 0, 6, 0 }, - { 6, 6, 0 }, - { 0, 6, 0 }, - }, - }, - O = { - { - { 0, 7, 7, 0 }, - { 0, 7, 7, 0 }, - { 0, 0, 0, 0 }, - }, - { - { 0, 7, 7, 0 }, - { 0, 7, 7, 0 }, - { 0, 0, 0, 0 }, - }, - { - { 0, 7, 7, 0 }, - { 0, 7, 7, 0 }, - { 0, 0, 0, 0 }, - }, - { - { 0, 7, 7, 0 }, - { 0, 7, 7, 0 }, - { 0, 0, 0, 0 }, - }, - }, - }, - - images = {}, - - loadImages = function(self) - self.images = { - Gfx.image.new('images/block2.png'), - Gfx.image.new('images/block6.png'), - Gfx.image.new('images/block7.png'), - Gfx.image.new('images/block4.png'), - Gfx.image.new('images/block5.png'), - Gfx.image.new('images/block1.png'), - Gfx.image.new('images/block3.png'), - ghost = Gfx.image.new('images/block0.png') - } - end, - - create = function(type, xStart, yStart) - local t = { - - type = type, - x = xStart, - y = yStart, - rotationIndex = 1, - isGhost = false, - - moveDown = function(self, stage) - if self:checkCollision(stage, 0, 1) then - return false - else - self.y = self.y + 1 - return true - end - end, - - moveLeft = function(self, stage) - if self:checkCollision(stage, -1, 0) then - return false - else - self.x = self.x - 1 - return true - end - end, - - moveRight = function(self, stage) - if self:checkCollision(stage, 1, 0) then - return false - else - self.x = self.x + 1 - return true - end - end, - - rotateClockwise = function(self, stage) - local newRotationIndex = self.rotationIndex + 1 - if newRotationIndex > 4 then - newRotationIndex = 1 - end - return self:rotate(stage, newRotationIndex) - end, - - rotateCounterClockwise = function(self, stage) - local newRotationIndex = self.rotationIndex - 1 - if newRotationIndex < 1 then - newRotationIndex = 4 - end - return self:rotate(stage, newRotationIndex) - end, - - rotate = function(self, stage, newRotationIndex) - if not self:checkCollision(stage, 0, 0, newRotationIndex) then - self.rotationIndex = newRotationIndex - return true - elseif not self:checkCollision(stage, 1, 0, newRotationIndex) then - self.x = self.x + 1 - self.rotationIndex = newRotationIndex - return true - elseif not self:checkCollision(stage, -1, 0, newRotationIndex) then - self.x = self.x - 1 - self.rotationIndex = newRotationIndex - return true - else - return false - end - end, - - checkCollision = function(self, stage, dx, dy, rotationIndex) - if not rotationIndex then - rotationIndex = self.rotationIndex - end - local pattern = Tetromino.pattern[self.type] - local rotation = pattern[rotationIndex] - for row = 1, #rotation do - for col = 1, #rotation[row] do - if rotation[row][col] > 0 then - local x = self.x + dx + col - 1 - local y = self.y + dy + row - 1 - if x < 1 then - return true - elseif x > stage.width then - return true - elseif y > stage.height then - return true - elseif stage.tiles[y][x] > 0 then - return true - end - end - end - end - return false - end, - - drawMino = function(self, tx, ty, blockIndex, drawBackground) - local x = tx * Tetromino.minoSize - local y = ty * Tetromino.minoSize - if drawBackground then - Gfx.setColor(Gfx.kColorWhite) - Gfx.fillRoundRect(x - 1, y - 1, Tetromino.minoSize + 1, Tetromino.minoSize + 1, 1) - Gfx.setColor(Gfx.kColorBlack) - end - if self.isGhost then - Tetromino.images.ghost:draw(x, y) - else - Tetromino.images[blockIndex]:draw(x, y) - end - end, - - draw = function(self, drawBackground) - local pattern = Tetromino.pattern[self.type] - local rotation = pattern[self.rotationIndex] - for row = 1, #rotation do - for col = 1, #rotation[row] do - if rotation[row][col] > 0 then - local x = self.x + col - 2 - local y = self.y + row - 4 - self:drawMino(x, y, rotation[row][col], drawBackground) - end - end - end - end, - - } - return t - end -} - -return tetromino \ No newline at end of file +local tetromino = { + minoSize = 10, + + types = { + "I", + "J", + "L", + "S", + "Z", + "T", + "O", + }, + + width = { + I = 4, + J = 3, + L = 3, + S = 3, + Z = 3, + T = 3, + O = 3, + }, + + pattern = { + I = { + { + { 0, 0, 0, 0 }, + { 1, 1, 1, 1 }, + { 0, 0, 0, 0 }, + { 0, 0, 0, 0 }, + }, + { + { 0, 0, 1, 0 }, + { 0, 0, 1, 0 }, + { 0, 0, 1, 0 }, + { 0, 0, 1, 0 }, + }, + { + { 0, 0, 0, 0 }, + { 0, 0, 0, 0 }, + { 1, 1, 1, 1 }, + { 0, 0, 0, 0 }, + }, + { + { 0, 1, 0, 0 }, + { 0, 1, 0, 0 }, + { 0, 1, 0, 0 }, + { 0, 1, 0, 0 }, + }, + }, + J = { + { + { 2, 0, 0 }, + { 2, 2, 2 }, + { 0, 0, 0 }, + }, + { + { 0, 2, 2 }, + { 0, 2, 0 }, + { 0, 2, 0 }, + }, + { + { 0, 0, 0 }, + { 2, 2, 2 }, + { 0, 0, 2 }, + }, + { + { 0, 2, 0 }, + { 0, 2, 0 }, + { 2, 2, 0 }, + }, + }, + L = { + { + { 0, 0, 3 }, + { 3, 3, 3 }, + { 0, 0, 0 }, + }, + { + { 0, 3, 0 }, + { 0, 3, 0 }, + { 0, 3, 3 }, + }, + { + { 0, 0, 0 }, + { 3, 3, 3 }, + { 3, 0, 0 }, + }, + { + { 3, 3, 0 }, + { 0, 3, 0 }, + { 0, 3, 0 }, + }, + }, + S = { + { + { 0, 4, 4 }, + { 4, 4, 0 }, + { 0, 0, 0 }, + }, + { + { 0, 4, 0 }, + { 0, 4, 4 }, + { 0, 0, 4 }, + }, + { + { 0, 0, 0 }, + { 0, 4, 4 }, + { 4, 4, 0 }, + }, + { + { 4, 0, 0 }, + { 4, 4, 0 }, + { 0, 4, 0 }, + }, + }, + Z = { + { + { 5, 5, 0 }, + { 0, 5, 5 }, + { 0, 0, 0 }, + }, + { + { 0, 0, 5 }, + { 0, 5, 5 }, + { 0, 5, 0 }, + }, + { + { 0, 0, 0 }, + { 5, 5, 0 }, + { 0, 5, 5 }, + }, + { + { 0, 5, 0 }, + { 5, 5, 0 }, + { 5, 0, 0 }, + }, + }, + T = { + { + { 0, 6, 0 }, + { 6, 6, 6 }, + { 0, 0, 0 }, + }, + { + { 0, 6, 0 }, + { 0, 6, 6 }, + { 0, 6, 0 }, + }, + { + { 0, 0, 0 }, + { 6, 6, 6 }, + { 0, 6, 0 }, + }, + { + { 0, 6, 0 }, + { 6, 6, 0 }, + { 0, 6, 0 }, + }, + }, + O = { + { + { 0, 7, 7, 0 }, + { 0, 7, 7, 0 }, + { 0, 0, 0, 0 }, + }, + { + { 0, 7, 7, 0 }, + { 0, 7, 7, 0 }, + { 0, 0, 0, 0 }, + }, + { + { 0, 7, 7, 0 }, + { 0, 7, 7, 0 }, + { 0, 0, 0, 0 }, + }, + { + { 0, 7, 7, 0 }, + { 0, 7, 7, 0 }, + { 0, 0, 0, 0 }, + }, + }, + }, + + images = {}, + + loadImages = function(self) + self.images = { + Gfx.image.new('images/block2.png'), + Gfx.image.new('images/block6.png'), + Gfx.image.new('images/block7.png'), + Gfx.image.new('images/block4.png'), + Gfx.image.new('images/block5.png'), + Gfx.image.new('images/block1.png'), + Gfx.image.new('images/block3.png'), + ghost = Gfx.image.new('images/block0.png') + } + end, + + create = function(type, xStart, yStart) + local t = { + + type = type, + x = xStart, + y = yStart, + rotationIndex = 1, + isGhost = false, + + moveDown = function(self, stage) + if self:checkCollision(stage, 0, 1) then + return false + else + self.y = self.y + 1 + return true + end + end, + + moveLeft = function(self, stage) + if self:checkCollision(stage, -1, 0) then + return false + else + self.x = self.x - 1 + return true + end + end, + + moveRight = function(self, stage) + if self:checkCollision(stage, 1, 0) then + return false + else + self.x = self.x + 1 + return true + end + end, + + rotateClockwise = function(self, stage) + local newRotationIndex = self.rotationIndex + 1 + if newRotationIndex > 4 then + newRotationIndex = 1 + end + return self:rotate(stage, newRotationIndex) + end, + + rotateCounterClockwise = function(self, stage) + local newRotationIndex = self.rotationIndex - 1 + if newRotationIndex < 1 then + newRotationIndex = 4 + end + return self:rotate(stage, newRotationIndex) + end, + + rotate = function(self, stage, newRotationIndex) + if not self:checkCollision(stage, 0, 0, newRotationIndex) then + self.rotationIndex = newRotationIndex + return true + elseif not self:checkCollision(stage, 1, 0, newRotationIndex) then + self.x = self.x + 1 + self.rotationIndex = newRotationIndex + return true + elseif not self:checkCollision(stage, -1, 0, newRotationIndex) then + self.x = self.x - 1 + self.rotationIndex = newRotationIndex + return true + else + return false + end + end, + + checkCollision = function(self, stage, dx, dy, rotationIndex) + if not rotationIndex then + rotationIndex = self.rotationIndex + end + local pattern = Tetromino.pattern[self.type] + local rotation = pattern[rotationIndex] + for row = 1, #rotation do + for col = 1, #rotation[row] do + if rotation[row][col] > 0 then + local x = self.x + dx + col - 1 + local y = self.y + dy + row - 1 + if x < 1 then + return true + elseif x > stage.width then + return true + elseif y > stage.height then + return true + elseif stage.tiles[y][x] > 0 then + return true + end + end + end + end + return false + end, + + drawMino = function(self, tx, ty, blockIndex, drawBackground) + local x = tx * Tetromino.minoSize + local y = ty * Tetromino.minoSize + if drawBackground then + Gfx.setColor(Gfx.kColorWhite) + Gfx.fillRoundRect(x - 1, y - 1, Tetromino.minoSize + 1, Tetromino.minoSize + 1, 1) + Gfx.setColor(Gfx.kColorBlack) + end + if self.isGhost then + Tetromino.images.ghost:draw(x, y) + else + Tetromino.images[blockIndex]:draw(x, y) + end + end, + + draw = function(self, drawBackground) + local pattern = Tetromino.pattern[self.type] + local rotation = pattern[self.rotationIndex] + for row = 1, #rotation do + for col = 1, #rotation[row] do + if rotation[row][col] > 0 then + local x = self.x + col - 2 + local y = self.y + row - 4 + self:drawMino(x, y, rotation[row][col], drawBackground) + end + end + end + end, + + } + return t + end +} + +return tetromino From 564db3ad1a7d835f90c4832c7a75a7697827f897 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 30 Sep 2023 22:16:17 -0700 Subject: [PATCH 2/4] Fixed slow speed issues amd changed maxLevel to 20 --- main.lua | 5 +++- stage.lua | 74 +++++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/main.lua b/main.lua index 2483fce..be500ef 100644 --- a/main.lua +++ b/main.lua @@ -72,9 +72,12 @@ local setup = function() Gfx.setDrawOffset(offsetX, offsetY) local menu = playdate.getSystemMenu() - ModeOption = menu:addOptionsMenuItem("mode", { "regular", "dynamic", "chill" }, currentStage.mode, function(option) + ModeOption = menu:addOptionsMenuItem("mode", { "marathon", "dynamic", "chill" }, currentStage.mode, function(option) currentStage:setMode(option) end) + for l = 1, Stage.maxLevel, 1 do + table.insert(Stage.levelStrings, string.format("%02d", l)) + end LevelOption = menu:addOptionsMenuItem("level", Stage.levelStrings, Stage.levelStrings[1], function(option) currentStage:setLevel(math.tointeger(option), true) end) diff --git a/stage.lua b/stage.lua index a9df376..f2da8cd 100644 --- a/stage.lua +++ b/stage.lua @@ -1,7 +1,28 @@ local stage = { - maxLevel = 10, - levelStrings = { "01", "02", "03", "04", "05", "06", "07", "08", "09", "10" }, - speeds = { 1000, 793, 618, 473, 355, 262, 190, 135, 94, 64 }, + maxLevel = 20, + levelStrings = {}, + -- These speeds are from https://tetris.wiki/Marathon + -- They are in rows per frame, at 60 frames per second + speeds = { 0.01667 * 60, + 0.021017 * 60, + 0.026977 * 60, + 0.035256 * 60, + 0.04693 * 60, + 0.06361 * 60, + 0.0879 * 60, + 0.1236 * 60, + 0.1775 * 60, + 0.2598 * 60, + 0.388 * 60, + 0.59 * 60, + 0.92 * 60, + 1.46 * 60, + 2.36 * 60, + 3.91 * 60, + 6.61 * 60, + 11.43 * 60, + 20 * 60 + }, create = function(startMusic, nextSong, stopMusic) local s = { @@ -32,11 +53,14 @@ local stage = { fourLinesCleared = 0, level = 1, - speed = 1000, - lockDelay = 500, - lockTimer = nil, tickTimer = nil, - holdDelay = 500, + tickRate = 10, -- in ms + speed = nil, + timeDropping = 0, + accumulatedTime = 0, + lockDelay = 500, -- in ms + lockTimer = nil, + holdDelay = 500, -- in ms isHardDropping = false, isWaitingForHoldLock = false, lastRowCleared = 1, @@ -47,7 +71,7 @@ local stage = { fillRow = 22, fillCol = 1, - mode = "dynamic", + mode = "marathon", levelLabelWidth = Gfx.getTextSize("LEVEL"), holdLabelWidth = Gfx.getTextSize("HOLD"), @@ -82,6 +106,7 @@ local stage = { }, setup = function(self) + self.speed = Stage.speeds[self.level] / self.tickRate self.isGameOver = false self.score = 0 @@ -112,7 +137,7 @@ local stage = { self:startMusic() - self.tickTimer = playdate.timer.new(self.speed, function() self:tick() end) + self.tickTimer = playdate.timer.new(self.tickRate, function() self:tick() end) self.tickTimer.repeats = true end, @@ -148,10 +173,7 @@ local stage = { self.level = level - self.speed = Stage.speeds[level] - if self.tickTimer then - self.tickTimer.duration = self.speed - end + self.speed = Stage.speeds[self.level] / self.tickRate if not setManually then LevelOption:setValue(Stage.levelStrings[level]) @@ -187,10 +209,13 @@ local stage = { y = 1 end + self.accumulatedTime = 0 self.tetromino = Tetromino.create(type, x, y) if self.tetromino:checkCollision(self, 0, 0) then self:gameOver() else + self.accumulatedTime = 0 + self.timeDropping = 0 if self.enableGhost then self.ghost = Tetromino.create(type, x, y) self.ghost.isGhost = true @@ -251,14 +276,14 @@ local stage = { startSoftDrop = function(self) if self.tetromino then self.soundEffects.softDrop:play(0) - self.tickTimer.duration = math.floor(self.speed / 20) + self.speed *= 20 end end, stopSoftDrop = function(self) if self.tetromino then self.soundEffects.softDrop:stop() - self.tickTimer.duration = self.speed + self.speed /= 20 end end, @@ -488,16 +513,26 @@ local stage = { tick = function(self) if self.tetromino then - local success = false + local dropped = false + local landed = false if self.isHardDropping then while self.tetromino:moveDown(self) do -- nothing, we're hard-dropping until we hit the bottom end else - success = self.tetromino:moveDown(self) + self.accumulatedTime += self.speed + -- Track time for when a tetronimo will decend more than one row per frame + if self.accumulatedTime > 1 then + -- If moveDown fails, we've landed + dropped = self.tetromino:moveDown(self) + if not dropped then + landed = true + end + self.accumulatedTime -= 1 + end end - if success then + if dropped then if self.lockTimer then self.lockTimer:remove() self.lockTimer = nil @@ -506,8 +541,9 @@ local stage = { local lockDelay = self.lockDelay if self.isHardDropping then lockDelay = 1 - else + elseif landed then self.soundEffects.land:play() + landed = false end self.lockTimer = playdate.timer.new(lockDelay, function() self:lock() end) end From 45d79ccced503396fa1bbf042b57c7e35ec220ee Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 30 Sep 2023 22:21:46 -0700 Subject: [PATCH 3/4] Fix slow speed and increase maxLevel to 20 --- images/block0.png | Bin 128 -> 1029 bytes images/block1.png | Bin 124 -> 1046 bytes images/block2.png | Bin 133 -> 1042 bytes images/block3.png | Bin 147 -> 1062 bytes images/block4.png | Bin 133 -> 1049 bytes images/block5.png | Bin 133 -> 1050 bytes images/block6.png | Bin 140 -> 1064 bytes images/block7.png | Bin 137 -> 1059 bytes stage.lua | 60 +++++++++++++++++++++++++++------------------- tetromino.lua | 2 +- 10 files changed, 37 insertions(+), 25 deletions(-) diff --git a/images/block0.png b/images/block0.png index 7e8628423188763ad3c68a2fa12dabce44384820..070d0627daa464252d9125b95b17a349b2820742 100644 GIT binary patch literal 1029 zcmZ`%&ui0g6n|Ma>ADUSe~k(HHNqHX%=E><$Z!Mz z26I_8k2*>vybu3ZYS+G@>Rn80X@ED62Y%~)cqXN6UIVx}0?@nz@C#kd?*KIc;Kv+* z`~={rv-a+?f)}riVy>)da2o9}^fALgBQPja%(0H0XX4O-e3*!Kxbp}EK8~N)! zoygJk(2kAwF!FXafvVrh&Uyd?2dOadYz@onam_-}FKUtK5m5bpFeE$vd&m$m3bL5Nkvgl6u&+n99_QuDc!FP ft?YkfMo%7qsar?Fv7hf#bYXKBrqx&H=WhQ2(p3zu literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4F%}28J29*~C-V}>vGR0r43U_6 z_QGy11_d7Hi?jdzw?BT6eNl+4tv6>2PwT1a-3+&t^Hmr%qJFwC7@k+Z)4gARo#Nle ao0(*|gxHci*PQ~I!r6Q!zZpM-^7cA*9ci6v!#`xgPKO8~#oRowxC($;aXy8Ik@XILV zi=mLSqbKu=T5*AvRmG+w8Qm<>qs%Z91fJuAA}5HvK=Zs97!iZR0PQsj+8s9XYi-TQ zUA6Xo8}Fg#?YDkZ$BfvF1<-kd2n8QkvAi}@OQr3!d`-;g1xCs0^D0v*7=#58C69K-p-9-5{lGn*5W(a8oKPa)qO z+crd&EtktoImqbd0viwnf#rCX=lzKATZ=_osrZZ5g?f;^I8oKgn3`d0dXXk^m3h5n zhkQPgsQESOEEvs1MXS*j_QzHfgAFhodl<>qa{J}|#UeL#n$uN1nS@Uxj+;0r-Aw6v zKK!52N$f%3e!)%*S?4CUuE3N!8NxCGsrcQC?#I zaULX$>jlG9EsGFF8u(V-ofBZ8PqE yYf4F%}28J29*~C-V}>G4phB43U_c zoYK(npJxZ7a?g+d|LvV*m6eslj`~#iI5BE)70zJWEL8G2{2 V3oY+m@Ed3XgQu&X%Q~loCIFngBt!rJ diff --git a/images/block2.png b/images/block2.png index cd4a5081b4236c67de6fdda4cac6bcfae77d0463..ac9d47e5228a4642135ae42bef8e3aaee409d9c9 100644 GIT binary patch literal 1042 zcmZ`%PiWI%6o2WqZtFHd=1{6=%ut5VBu(8$LR#q7%~s5qrAQ|pmbK{?Y?F|rwi6GX z_=kZwP=p}^3tkjq%#$FV{BsvPdh(=r+f{`5zO?J=oPn43zW4op@6Y$%>g>#gfj}?- z00XIHB8|GANRJ=?SIXDEqv~9YD{+9=Ykj}fE(Ggs z;P0W3FNs3Vo}Ml)>&7CjXsS!cbEa+3K z-PelTU+>tn@g9b_Goz?_t>l~o&^Jhgf~Ol;p3l}ZSvRX(l5%E=RrBV8##T!fVS!wg z&@5@LN>@uo!;z{{#zRPG6F0}u9>l#KWwOdFJ!#q+J;w4Z&%^>WP0Mz^Af*%2O*)>U z%#!O`631046}A##O?#0Gi=xQ!0w)L|M1-7W!&R#x!x?S_*^QIXoSdy&u5KDMiK{M{ zWjD$&BvI>Y)>*Pzi43RN752wfRf`L=Ja-Vu)eC#&{ly|T4Vu?gBbkg(BTd>kDAUfE zW-<1k(aGF?;9kLA48_drh2@0mYB4h95HE%VF|#}62E7f_#wK-1b7{>ni)HdGv2j7> z{&DUnOqwOj)*OcrMw|F{-+p}dpakvCCPC)NRNAK0SOL$AI}i(CE4%v?3%q-|O;&d% zm566XhI08@ZD%%k>Qm37&ciz^@N8o1#PDz;Iv9Vl6!5S3Uwr*`==tX3&r0{)2j82% v)fMIRrQj?7t!>}Kfx7WN+`XD#pZfR(*2a2I-ncvRfNXDSY9_HcF@O6Hd+re! literal 133 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4F%}28J29*~C-V}>vG;Uw43U`H z+ixhupullB>iqxuoqVSjxF6*4*~xIk?Sny^%KM48rCnbh>RNuKJO1c)Hipf@K^y_#e%mUP5J02gM-3quI66xb{_ z^2<&2$j-8D-Nt)3xls9uYKs;gGXUDQ5uxDDJeJp_XJT(=CX<*6#Z@sA)rm%tedtV=2^cxE{@OxsxH$98IIxH0S8UfK7Ar3L_>o$ zI-dOQGp4BtEL$iPm_iSu>S?yy>-Dl6&+@zn5gubwF{PqMG4@o0ti=h*MpDmcW=2(L z5?4y7dDHK9lSK8eR%cGDCsK@BSJ)q0lr*-R;n>YcW+t^>-d`+oQ>9s5Rg?MfX@mhC z2c_yURm}$eGddr;5x8Ek6+=`fGO5XsWXb_DWe?}|@ZQ+kl&kaxOaq(LC5%O7L(S&N zvjqBiAN!AUBVjf4F%}28J29*~C-V}>@%40Z43U`H z+waJAz<}rQlE44Y-&@F?@;2&Ox1yNY#DyECGlV;yOh{jUr!bm3bkYK*imM+P0x#D- vV?FC#`OJQ={jOH2Kl?ATy?>$@UCO+=JmMtd`Zari1~YiN`njxgN@xNA9v?O4 diff --git a/images/block4.png b/images/block4.png index ff0c258435f31f5479320c0f9559a682d37c04fa..ac6ffc325cc802dcfd8ea12f2d9ef8ff55419b6a 100644 GIT binary patch literal 1049 zcmZ`%PiWI%6o2V9o!tiFoRjSm;vYn5lBVuYLR#onXRBsxwT^CjSk|U9v&|Qily)BG z9OCdK9z>BLMLmk>bsz|$2SImG^e{a8@1pY}BFyhgyROa|czN%8-|zSSeD7Ty8a&$K z?eGFXODr0RW8F-oaSQ&Bs(v!8gaIC3Yx=D=;4__y#ub1I`vGPz1N_3S*-rp@ z9^lJ3fOHF>-CS54mhr_SEg8!w3LM0CBQ#JQKq63Bj!~UeJ5Kq*gMGWPj8TGX@5Ht> zuR}f71b+n!`J!0J+0_HtY2BEl6-{;Ma0*!l-NPPc10e81K;lJ75NSb>0$q~O4NzC2 zpw4Aue#ujf++3<#xA8s9`3G@tu+5B)+5kB@oJn{xdq6+X!4Q*o~o}w4RxcsE!sQQ}*$qPY@GpQ!dkMFg0vamoysJY?RHD zX9@KPGWU;jBcUJVOiQzELg=dCYkeE>$;}efIx7U3BU7oFQh5bjFW!NW7q_yuPa*I7 zgv+o7vK4nyYuh#uT1pA%crfS>#y1Z?VdY7lD+92 z)Oj>Db9(+wo7{ K!N|kD@wq>9fE!W( literal 133 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4F%}28J29*~C-V}>vG;Uw43U_c zoYK(npJxZ7a?g+d|Lt95m6etKj`|dc<+&ClKfHFx=hz~_L?uOsElwhB7de e>=8d-2?IlC2)AIYyGb(8C_kBN~_v8EC z0AU^AjJ@&prid=KwPLm`%WxUlJ$x(WkP5{Sp34!GWjwd-zNQ?>b5rE((0YRUQ z_?v-tX@CGbpd+&u@LZL1CCn+BxARD_Giu57gog`J1bL52OXygdZ(kztq`4(Ww>{l5NgP*M zu&Q2?qHv=2*Q(R7+lfrK)fMWGsVg=Ur&;DGlBbss%KM9jZ<;i}t7bA0J&iEopg~zq z!Lll;|BOy#4g(Jg_G8GFs+X4~#nV!F$}yIYaeQHa%1wF)rh|>^5@z$7YgMZFSyH2% z$o%6xOqj3?+tFMX6Rx!Ioxa2P;!z1Yoh^dM;Hh*>sks8a7wteQj9S^>r&RdS*5Djk zv6-xtE?hcYQX7rETy*eV&$G~py?gL-eEa;+kd*9CKVJ%Gc6V0Fu+du$tgro4KNwd> z&Y2fZuBro>-NqMPJx;v|eGI+telj)om3$icoEZ+nP9+i*KCPDU7H6+bORvV~SN;H} CNE6Zk literal 133 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4F%}28J29*~C-V}>vG;Uw43U_c zoYK(npJxZ7a?g+d|LyZidwK*)bu3gAt|>?!XgTgB!eB0VQzfS%L)de>)co_35(^TK eIJj5{@iG)n;Spn5esm4cC{- zSftttLahgpQXwb7L&aX}MZ^}7>cLwN;;H^o@gN9_pz}6q8f$&*?97|@eKYgk%*~<5 zK(nL60RYY6P%w(RiAaMT|L1dKA5hgz2E+is%6#K5sUG)KA`}$?t{erJo&orYuIY~e zIS%0SIDl{u;E=YoI_$-R$8tQJ5=H1jy8-HL9Y7-3P|ny6SL~>*8*0$kjxuWFExQ$M zeKrkss0scC3Rxi(vbS`9W-6^rQlc!GR3M@13gu>c7#Hvy?-DqVz<)wKfEF4t02&VxvBBaJmRF;vV{tPs4hji1%ScIeLS_nC zjj+I55YWuZrbHF88O0C^KH5SEXcIR}Qx?R$?4#r25Y?yZGIfIC7>@QkD2npx$&?Tc z_Lu3n^U;?~QxjM=pU*RS7o+NvY`4ebVL6`Vc_$*A#*|`81*c+kmV)fW3Cc!7Pitmc zRVWfynox75kEThY$|~2H)hdY;qkJp8A6t+#wwvMD{YYjywOig_EOJt!S+^=B^WxJ8 zeL6Nu)nlrf@&BiFUUn~Vw_q!Vs3y~?si0)ae$r(p=W+6$*iM&A^eRjho75$YL}f$G zHVi^IUdC7Z_TuCFC8&0m30{_TrK(G%5wN^C1AYfyWoMrJ zjxQ${mT|f4F%}28J29*~C-V}>ar1O>43U^R zx6e|DL4k)!@&A8gvA$;)B6i41DhLQ4eCT?Z!K3SY%B@|?tTVm_-TM6hs+6zFe+OWPF)`gGpo>R}6itdE971+2RJ)o0M{prIG#rc{-1fH1yZJd&d;9nD{Pu<1{@tN>$xU5?@5a*N&0WVnPD$719@h{% zU+tM1zW4I|{no>;N5>xIk9=O3shayRI;%vVdV0mn()7%kf4F%}28J29*~C-V}>arSg^43U_+ zcY-G$g8~P$^}qk;D;${fE>14a@?_>cVaS@ZhyQF6U&gmd8naF;JR_7NEf#ync+S~I kf7!<5<{9 diff --git a/stage.lua b/stage.lua index f2da8cd..1c0f297 100644 --- a/stage.lua +++ b/stage.lua @@ -600,50 +600,62 @@ local stage = { self.scoreDisplay = math.min(self.score, self.scoreDisplay + 10) end end - Gfx.drawText("SCORE", displayWidth + 10, displayHeight - 26) - Gfx.drawText(self.scoreDisplay, displayWidth + 10, displayHeight - 10) - Gfx.drawText("LEVEL", -self.levelLabelWidth - 10, displayHeight - 26) + local _, pixOffset, _, _ = Gfx.getClipRect() + pixOffset = math.abs(pixOffset) + + Gfx.drawText("SCORE", displayWidth + 10, playdate.display.getHeight() - pixOffset - 46) + Gfx.drawText(self.scoreDisplay, displayWidth + 10, playdate.display.getHeight() - pixOffset - 30) + + Gfx.drawText("LEVEL", -self.levelLabelWidth - 10, playdate.display.getHeight() - pixOffset - 46) self.levelTextWidth = Gfx.getTextSize(self.level) - Gfx.drawText(self.level, -self.levelTextWidth - 10, displayHeight - 10) + Gfx.drawText(self.level, -self.levelTextWidth - 10, playdate.display.getHeight() - pixOffset - 30) if self.enablePreview then playdate.graphics.setImageDrawMode(playdate.graphics.kDrawModeFillWhite) - Gfx.drawText("NEXT", displayWidth + 9, -1) - Gfx.drawText("NEXT", displayWidth + 9, 0) - Gfx.drawText("NEXT", displayWidth + 9, 1) - Gfx.drawText("NEXT", displayWidth + 11, -1) - Gfx.drawText("NEXT", displayWidth + 11, 0) - Gfx.drawText("NEXT", displayWidth + 11, 1) - Gfx.drawText("NEXT", displayWidth + 10, -1) - Gfx.drawText("NEXT", displayWidth + 10, 1) + Gfx.drawText("NEXT", displayWidth + 9, 20 - pixOffset - 1) + Gfx.drawText("NEXT", displayWidth + 9, 20 - pixOffset) + Gfx.drawText("NEXT", displayWidth + 9, 20 - pixOffset + 1) + Gfx.drawText("NEXT", displayWidth + 11, 20 - pixOffset - 1) + Gfx.drawText("NEXT", displayWidth + 11, 20 - pixOffset) + Gfx.drawText("NEXT", displayWidth + 11, 20 - pixOffset + 1) + Gfx.drawText("NEXT", displayWidth + 10, 20 - pixOffset - 1) + Gfx.drawText("NEXT", displayWidth + 10, 20 - pixOffset + 1) playdate.graphics.setImageDrawMode(playdate.graphics.kDrawModeCopy) - Gfx.drawText("NEXT", displayWidth + 10, 0) + Gfx.drawText("NEXT", displayWidth + 10, 20 - pixOffset) end if self.enableHold then playdate.graphics.setImageDrawMode(playdate.graphics.kDrawModeFillWhite) - Gfx.drawText("HOLD", -self.holdLabelWidth - 9, -1) - Gfx.drawText("HOLD", -self.holdLabelWidth - 9, 0) - Gfx.drawText("HOLD", -self.holdLabelWidth - 9, 1) - Gfx.drawText("HOLD", -self.holdLabelWidth - 11, -1) - Gfx.drawText("HOLD", -self.holdLabelWidth - 11, 0) - Gfx.drawText("HOLD", -self.holdLabelWidth - 11, 1) - Gfx.drawText("HOLD", -self.holdLabelWidth - 10, -1) - Gfx.drawText("HOLD", -self.holdLabelWidth - 10, 1) + Gfx.drawText("HOLD", -self.holdLabelWidth - 9, 20 - pixOffset - 1) + Gfx.drawText("HOLD", -self.holdLabelWidth - 9, 20 - pixOffset) + Gfx.drawText("HOLD", -self.holdLabelWidth - 9, 20 - pixOffset + 1) + Gfx.drawText("HOLD", -self.holdLabelWidth - 11, 20 - pixOffset - 1) + Gfx.drawText("HOLD", -self.holdLabelWidth - 11, 20 - pixOffset) + Gfx.drawText("HOLD", -self.holdLabelWidth - 11, 20 - pixOffset + 1) + Gfx.drawText("HOLD", -self.holdLabelWidth - 10, 20 - pixOffset - 1) + Gfx.drawText("HOLD", -self.holdLabelWidth - 10, 20 - pixOffset + 1) playdate.graphics.setImageDrawMode(playdate.graphics.kDrawModeCopy) - Gfx.drawText("HOLD", -self.holdLabelWidth - 10, 0) + Gfx.drawText("HOLD", -self.holdLabelWidth - 10, 20 - pixOffset) end if self.enablePreview and self.preview then self.preview.x = self.width + 2 - self.preview.y = 5 + if (pixOffset > 0) then + self.preview.y = 5 + else + self.preview.y = 6 + end self.preview:draw(true) end if self.enableHold and self.hold then self.hold.x = -Tetromino.width[self.hold.type] - self.hold.y = 5 + if (pixOffset > 0) then + self.hold.y = 5 + else + self.hold.y = 6 + end self.hold:draw(true) end diff --git a/tetromino.lua b/tetromino.lua index e1d776b..48d20e9 100644 --- a/tetromino.lua +++ b/tetromino.lua @@ -1,5 +1,5 @@ local tetromino = { - minoSize = 10, + minoSize = 12, types = { "I", From 7db075163a76e42e1b4680286971726539ee0fc5 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 30 Sep 2023 22:43:27 -0700 Subject: [PATCH 4/4] Added wrapping image, and remvoed buggy level update --- images/wrapping-pattern.png | Bin 0 -> 9955 bytes stage.lua | 4 ---- 2 files changed, 4 deletions(-) create mode 100644 images/wrapping-pattern.png diff --git a/images/wrapping-pattern.png b/images/wrapping-pattern.png new file mode 100644 index 0000000000000000000000000000000000000000..ab295ce6965822ca6902bce7f5f74a2bc1c0118c GIT binary patch literal 9955 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