Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- SCORE_Y = 1 --Y value to draw the score text; it'll be centered on the X axis
- CONTROLS_Y = 18 --Y value to draw RESET button; it'll be centered on the X axis
- ANIMATION_TIME = 0.1--Animation sleep time, lower is faster
- m = nil --Monitor peripheral, if connected
- function getMonitor()
- --Sets m to equal an attached monitor if available and large enough.
- --Returns true if a monitor is found
- local i = 1
- local sides = rs.getSides() --Returns a list of sides (e.g. "left", "back", etc)
- while i <= #sides and m == nil do --Loop every side or until a monitor is found
- if peripheral.getType(sides[i]) == "monitor" then
- local w, h = peripheral.call(sides[i], "getSize")
- if w >= 18 and h >= 19 then --Attached monitor must be at least 18x19
- print("Monitor detected. Outputting game to "..sides[i].." monitor.")
- m = peripheral.wrap(sides[i])
- return true
- else
- print("Monitor size "..w.."x"..h.." detected but at least 18x19 is required.")
- print("Press any key to continue...")
- sapphUtils.pullEvent("key") --Wait for a keypress
- term.clear()
- end
- end
- i = i + 1
- end
- return false
- end
- function drawScreen()
- --Draws all screen elements. Use drawScore() and drawGrid() to refresh just those
- m.setBackgroundColor(colors.black)
- m.clear()
- --Draw the border
- m.setBackgroundColor(colors.white)
- for x = tlGridX - 1, tlGridX + 16 do --Top & bottom borders
- m.setCursorPos(x, tlGridY - 1)
- m.write(" ")
- m.setCursorPos(x, tlGridY - 1 + 13)
- m.write(" ")
- end
- for y = tlGridY - 1, tlGridY + 12 do --Left & right borders
- m.setCursorPos(tlGridX - 1, y)
- m.write(" ")
- m.setCursorPos(tlGridX - 1 + 17, y)
- m.write(" ")
- end
- if isValidMonitorConnected then
- --Draw reset button
- m.setBackgroundColor(colors.orange)
- m.setTextColor(colors.white)
- m.setCursorPos(termWidth / 2 - string.len("RESET") / 2 + 1, CONTROLS_Y)
- m.write("RESET")
- --Draw quit "X" at top right
- m.setBackgroundColor(colors.red)
- m.setCursorPos(termWidth, 1)
- m.write("X")
- else
- --Draw control description
- m.setBackgroundColor(colors.black)
- m.setTextColor(colors.white)
- m.setCursorPos(1, 8)
- m.write("Controls:")
- m.setCursorPos(1, 9)
- m.write("Arrow keys")
- m.setCursorPos(1, 10)
- m.write("(R)eset")
- m.setCursorPos(1, 11)
- m.write("Switch site - X")
- end
- drawScore()
- drawGrid()
- end
- function drawScore()
- m.setBackgroundColor(colors.black)
- m.setTextColor(colors.white)
- scoreText = "Score: "..score
- m.setCursorPos(termWidth / 2 - string.len(scoreText) / 2 + 1, SCORE_Y) --Center the score on the X axis
- m.write(scoreText)
- end
- function drawGrid()
- for r = 0, 3 do --Row
- for c = 0, 3 do --Column
- drawSquare(r, c, grid[r][c])
- end
- end
- end
- function drawSquare(r, c, value, randomColor)
- --Draws an individual square on the board
- --Board squares are 4 wide and 3 tall so multiply c & r values respectively
- --randomColor and -1 value used for intro sequence only
- if value == 0 then --Black out the square
- m.setBackgroundColor(colors.black)
- m.setCursorPos(c * 4 + tlGridX, r * 3 + tlGridY)
- m.write(" ")
- m.setCursorPos(c * 4 + tlGridX, r * 3 + 1 + tlGridY)
- m.write(" ")
- m.setCursorPos(c * 4 + tlGridX, r * 3 + 2 + tlGridY)
- m.write(" ")
- elseif value == -1 then --Only used for rendering the 0, which normally is invisible, during the intro sequence
- if not randomColor then
- m.setBackgroundColor(colors.green)
- else
- m.setBackgroundColor(2^math.random(2, 14)) --Any color but white or black
- end
- m.setCursorPos(c * 4 + tlGridX, r * 3 + tlGridY)
- m.write(" ")
- m.setCursorPos(c * 4 + tlGridX, r * 3 + 1 + tlGridY)
- m.write(" 0 ")
- m.setCursorPos(c * 4 + tlGridX, r * 3 + 2 + tlGridY)
- m.write(" ")
- else
- if not randomColor then
- m.setBackgroundColor(value)
- else
- m.setBackgroundColor(2^math.random(2, 14)) --Any color but white or black
- end
- m.setCursorPos(c * 4 + tlGridX, r * 3 + tlGridY)
- m.write(" ")
- m.setCursorPos(c * 4 + tlGridX, r * 3 + 1 + tlGridY)
- --Center the number string within spaces (e.g. " 2 " or " 32 ")
- local strlen = string.len(value)
- if strlen == 1 then
- m.write(" "..value.." ")
- elseif strlen == 2 then
- m.write(" "..value.." ")
- elseif strlen == 3 then
- m.write(" "..value)
- else
- m.write(tostring(value))
- end
- m.setCursorPos(c * 4 + tlGridX, r * 3 + 2 + tlGridY)
- m.write(" ")
- end
- end
- function clearGrid()
- --Initializes the grid
- for r = 0, 3 do
- grid[r] = {}
- for c = 0, 3 do
- grid[r][c] = 0
- end
- end
- end
- function placeNewTile()
- --Places a 2 or 4 at a random empty location in the grid
- local number
- if math.random(10) == 10 then --Place a 4 10% of the time, else 2
- number = 4
- else
- number = 2
- end
- --Get a count of empty cells in the grid
- local emptyCount = 0
- for r = 0, 3 do
- for c = 0, 3 do
- if grid[r][c] == 0 then
- emptyCount = emptyCount + 1
- end
- end
- end
- --Choose an empty cell and place the number
- local target = math.random(emptyCount)
- emptyCount = 0
- for r = 0, 3 do
- for c = 0, 3 do
- if grid[r][c] == 0 then
- emptyCount = emptyCount + 1
- if emptyCount == target then
- grid[r][c] = number
- end
- end
- end
- end
- end
- function transpose()
- --[[ Turns rows into columns and vice-versa, like so:
- 0, 1, 2, 3, 0, 4, 8, C,
- 4, 5, 6, 7, 1, 5, 9, D,
- 8, 9, A, B, becomes 2, 6, A, E,
- C, D, E, F 3, 7, B, F
- ]]--
- local res = {}
- for r = 0, 3 do
- res[r] = {}
- for c = 0, 3 do
- res[r][c] = grid[c][r]
- end
- end
- grid = res
- end
- function rotateClockwise()
- transpose() --Transposing then reversing the order of the rows is equivalent to rotating the grid clockwise
- reverseRows()
- end
- function rotateCounterClockwise()
- transpose() --Transposing then reversing the order of the columns is equivalent to rotating the grid counterclockwise
- reverseColumns()
- end
- function reverseRows()
- for r = 0, 3 do
- grid[r][0], grid[r][3] = grid[r][3], grid[r][0] --Swap the first and fourth values
- grid[r][1], grid[r][2] = grid[r][2], grid[r][1] --Swap the second and third values
- end
- end
- function reverseColumns()
- for c = 0, 3 do
- grid[0][c], grid[3][c] = grid[3][c], grid[0][c] --Swap the first and fourth values
- grid[1][c], grid[2][c] = grid[2][c], grid[1][c] --Swap the second and third values
- end
- end
- function collapseLeft()
- --For each empty (0) cell, shift the next non-zero number into the cell
- --For each cell with a number, merge with an equal number to the right if possible
- local hadMove = false
- local tempC
- for r = 0, 3 do --For each row
- for c = 0, 3 do --For each column in row r
- if grid[r][c] == 0 then
- --Find the next non-zero and shift it here
- tempC = c + 1
- while tempC < 4 and grid[r][tempC] == 0 do
- tempC = tempC + 1
- end
- if tempC < 4 then
- hadMove = true
- grid[r][c] = grid[r][tempC]
- grid[r][tempC] = 0
- end
- end
- --Check for merges
- --If the next non-zero in the row matches this cell then merge
- tempC = c + 1
- while tempC < 4 and grid[r][tempC] == 0 do
- tempC = tempC + 1
- end
- if grid[r][c] == grid[r][tempC] then
- hadMove = true
- score = score + grid[r][c] + grid[r][tempC]
- grid[r][c] = grid[r][c] + grid[r][tempC]
- grid[r][tempC] = 0
- end
- end
- end
- return hadMove
- end
- function isGameOver()
- --If there are any zeroes in the grid then there's a valid move
- --Otherwise, if the cell to the right or underneath the current
- -- cell has the same value then a merge is possible.
- local r = 0
- while r < 4 do
- local c = 0
- while c < 4 do
- if grid[r][c] == 0 then
- return false
- elseif r < 3 and grid[r][c] == grid[r+1][c] then
- return false
- elseif c < 3 and grid[r][c] == grid[r][c+1] then
- return false
- end
- c = c + 1
- end
- r = r + 1
- end
- return true
- end
- function moveLeft()
- return collapseLeft()
- end
- function moveRight()
- --Mirror the grid so it can be collapsed to the left, then mirror it back
- reverseRows()
- hadMove = collapseLeft()
- reverseRows()
- return hadMove
- end
- function moveDown()
- --Rotate the grid so it's down becomes left, collapse, revert
- rotateClockwise()
- hadMove = collapseLeft()
- rotateCounterClockwise()
- return hadMove
- end
- function moveUp()
- --Rotate the grid so it's up becomes left, collapse, revert
- rotateCounterClockwise()
- hadMove = collapseLeft()
- rotateClockwise()
- return hadMove
- end
- function introShiftColumns(column, counter)
- --Weird, hacky code to animate the intro sequence.
- counter = counter - column
- if counter >= 0 and counter < 3 then
- grid[counter + 1][column] = grid[counter][column]
- grid[counter][column] = 0
- elseif counter >= 0 and counter < 5 then
- grid[5 - counter][column] = grid[5 - counter + 1][column]
- grid[5 - counter + 1][column] = 0
- end
- end
- function intro()
- --This is a bit ugly
- clearGrid()
- for counter = 0, 8 do
- if counter == 0 then
- grid[0][0] = 2
- elseif counter == 1 then
- grid[0][1] = -1
- elseif counter == 2 then
- grid[0][2] = 4
- elseif counter == 3 then
- grid[0][3] = 8
- end
- drawScreen(true)
- sleep(ANIMATION_TIME)
- for i = 0, 3 do
- introShiftColumns(i, counter)
- end
- end
- --Color cycle
- for counter = 0, 10 do
- drawSquare(1, 0, 2, true)
- drawSquare(1, 1, -1, true)
- drawSquare(1, 2, 4, true)
- drawSquare(1, 3, 8, true)
- sleep(ANIMATION_TIME)
- end
- m.setBackgroundColor(colors.black)
- if isValidMonitorConnected then
- m.setCursorPos(termWidth / 2 - string.len("Touch to begin") / 2 + 1, tlGridY + 7)
- m.write("Touch to begin")
- m.setCursorPos(tlGridX + 1, tlGridY + 9)
- m.write("Controls: Click")
- m.setCursorPos(tlGridX + 1, tlGridY + 10)
- m.write("a side to move.")
- _, _, x, y = sapphUtils.pullEvent("monitor_touch") --Wait for click after intro
- if x == termWidth and y == 1 then --If quit was pressed
- while true do sapphUtils.pullEvent() end --Ghetto quit
- end
- else
- m.setCursorPos(termWidth / 2 - string.len("Press any key...") / 2 + 1, tlGridY + 7)
- m.write("Press any key...")
- _, keycode = sapphUtils.pullEvent("key") --Wait for keypress after intro
- if keys.getName(keycode) == "x" then --If quit was pressed
- m.setBackgroundColor(colors.black)
- m.clear()
- while true do sapphUtils.pullEvent() end --Ghetto quit
- end
- end
- end
- --
- --MAIN GAME LOOP
- --
- grid = {} --Main 2D array for the game grid
- score = 0 --Score is the sum of all merges performed during a game
- isValidMonitorConnected = getMonitor() --Detect a monitor for output instead of the term if one is connected
- if isValidMonitorConnected then
- termWidth, termHeight = m.getSize()
- tlGridX = termWidth / 2 - 7 --Top left X coord of the game grid (not including borders)
- tlGridY = 3 --Top left Y coord of the game grid (not including borders)
- else
- m = term --Use the computer's terminal as output
- termWidth, termHeight = term.getSize()
- tlGridX = termWidth / 2 - 7
- tlGridY = 3
- end
- if not m.isColor() then --Color monitors/computers only
- print("An advanced monitor or computer is required.")
- print("Press any key to exit...")
- sapphUtils.pullEvent("key")
- return
- end
- while true do
- intro() --Comment this out to disable the intro. I won't judge
- clearGrid() --Initialize the game board array
- --Place the initial two tiles
- score = 0
- placeNewTile()
- placeNewTile()
- drawScreen()
- --Begin main round loop
- gameOver = false
- hadMove = false
- while not gameOver do
- hadMove = false
- if isValidMonitorConnected then --Use touch if a monitor is being used
- --Detect a click event for the buttons
- _, _, xT, yT = sapphUtils.pullEvent("monitor_touch")
- --If a direction button was pressed
- if yT >= tlGridY - 1 and yT <= tlGridY + 1 and xT >= tlGridX + 4 and xT <= tlGridX + 11 then
- hadMove = moveUp()
- elseif yT >= tlGridY + 9 and yT <= tlGridY + 12 and xT >= tlGridX + 4 and xT <= tlGridX + 11 then
- hadMove = moveDown()
- elseif yT >= tlGridY + 3 and yT <= tlGridY + 8 and xT >= tlGridX - 1 and xT <= tlGridX + 3 then
- hadMove = moveLeft()
- elseif yT >= tlGridY + 3 and yT <= tlGridY + 8 and xT >= tlGridX + 12 and xT <= tlGridX + 16 then
- hadMove = moveRight()
- elseif yT == CONTROLS_Y and xT >= (termWidth / 2 - string.len("RESET") / 2 + 1) and xT <= termWidth / 2 - string.len("RESET") / 2 + 1 + string.len("RESET") then
- hadMove = false
- gameOver = true --Reset button hit
- elseif yT == 1 and xT == termWidth then
- m.setBackgroundColor(colors.black)
- m.clear()
- return --Quit
- end
- else --Use keyboard controls if playing directly on a computer
- _, keycode = os.pullEvent("key")
- if keys.getName(keycode) == "up" then
- hadMove = moveUp()
- elseif keys.getName(keycode) == "down" then
- hadMove = moveDown()
- elseif keys.getName(keycode) == "left" then
- hadMove = moveLeft()
- elseif keys.getName(keycode) == "right" then
- hadMove = moveRight()
- elseif keys.getName(keycode) == "r" then --Reset
- hadMove = false
- gameOver = true
- elseif keys.getName(keycode) == "x" then --Quit
- m.setBackgroundColor(colors.black)
- m.clear()
- m.setCursorPos(1,1)
- return --Quit
- end
- end
- if hadMove then
- placeNewTile()
- drawGrid()
- drawScore()
- end
- if isGameOver() then
- gameOver = true
- end
- end
- --Draw game over
- m.setTextColor(colors.white)
- m.setBackgroundColor(colors.red)
- m.setCursorPos(termWidth / 2 - string.len("GAME OVER") / 2 + 1, CONTROLS_Y - 1)
- m.write("GAME OVER")
- m.setBackgroundColor(colors.black)
- if isValidMonitorConnected then
- m.setCursorPos(termWidth / 2 - string.len("Touch to reset...") / 2 + 1, CONTROLS_Y)
- m.write("Touch to reset...")
- --Wait for any click to continue
- sapphUtils.pullEvent("monitor_touch")
- else
- m.setCursorPos(termWidth / 2 - string.len("Press any key...") / 2 + 1, CONTROLS_Y)
- m.write("Press any key...")
- --Wait for any click to continue
- sapphUtils.pullEvent("key")
- end
- end
- while true do
- sapphUtils.pullEvent()
- --keeps sapphari on "life support"
- end
Advertisement
Add Comment
Please, Sign In to add comment