var core = use('core') var camera = use('camera') var compositor = use('compositor') var input = use('input') var shape = use('shape2d') var text2d = use('text2d') var random = use('random') var COLS = 10, ROWS = 20 var TILE = 8 var GW = COLS * TILE + 80 // extra space for next piece + score var GH = ROWS * TILE var game_cam = camera.make({width: GW, height: GH, pos: {x: GW / 2, y: GH / 2}}) var hud_cam = camera.make({width: GW, height: GH, pos: {x: GW / 2, y: GH / 2}}) // Action mapping with possession for discrete inputs input.configure({ action_map: { left: ['a', 'left'], right: ['d', 'right'], rotate: ['w', 'up'], soft_drop: ['s', 'down'], hard_drop: ['space'] } }) // Tetrimino definitions var SHAPES = { I: {color: {r: 0, g: 1, b: 1, a: 1}, blocks: [[0, 0], [1, 0], [2, 0], [3, 0]]}, O: {color: {r: 1, g: 1, b: 0, a: 1}, blocks: [[0, 0], [1, 0], [0, 1], [1, 1]]}, T: {color: {r: 1, g: 0, b: 1, a: 1}, blocks: [[0, 0], [1, 0], [2, 0], [1, 1]]}, S: {color: {r: 0, g: 1, b: 0, a: 1}, blocks: [[1, 0], [2, 0], [0, 1], [1, 1]]}, Z: {color: {r: 1, g: 0, b: 0, a: 1}, blocks: [[0, 0], [1, 0], [1, 1], [2, 1]]}, J: {color: {r: 0, g: 0, b: 1, a: 1}, blocks: [[0, 0], [0, 1], [1, 1], [2, 1]]}, L: {color: {r: 1, g: 0.5, b: 0, a: 1}, blocks: [[2, 0], [0, 1], [1, 1], [2, 1]]} } var shapeKeys = array(SHAPES) // Board: 2D array, null or color object var board = [] // Board shapes: one shape per cell, toggled visible var board_shapes = [] function init_board() { board = [] board_shapes = [] var r = 0, c = 0, row = null, srow = null for (r = 0; r < ROWS; r++) { row = [] srow = [] for (c = 0; c < COLS; c++) { row[] = null srow[] = shape.rect({ pos: {x: c * TILE + TILE / 2, y: r * TILE + TILE / 2}, width: TILE - 1, height: TILE - 1, fill: {r: 0.5, g: 0.5, b: 0.5, a: 1}, plane: 'game', layer: 0, visible: false }) } board[] = row board_shapes[] = srow } } // Active piece var piece = null, pieceX = 0, pieceY = 0 var piece_shapes = [] var nextPiece = null // Score var score = 0, linesCleared = 0, level = 0 var gameOver = false // Gravity var baseGravity = 0.8 var gravityTimer = 0 var softDropping = false // Horizontal DAS var hMoveTimer = 0 var hDelay = 0.18 var hRepeat = 0.05 var hDir = 0 var hHeld = false // Rotate lock var rotateHeld = false var score_label = text2d({ text: "Score: 0", pos: {x: COLS * TILE + 5, y: GH - 15}, plane: 'hud', size: 8, color: {r: 1, g: 1, b: 1, a: 1} }) var level_label = text2d({ text: "Level: 0", pos: {x: COLS * TILE + 5, y: GH - 30}, plane: 'hud', size: 8, color: {r: 1, g: 1, b: 1, a: 1} }) var next_label = text2d({ text: "Next:", pos: {x: COLS * TILE + 5, y: 50}, plane: 'hud', size: 8, color: {r: 1, g: 1, b: 1, a: 1} }) // Next piece display shapes var next_shapes = [] var ns = 0 for (ns = 0; ns < 4; ns++) { next_shapes[] = shape.rect({ pos: {x: 0, y: 0}, width: TILE - 1, height: TILE - 1, fill: {r: 1, g: 1, b: 1, a: 1}, plane: 'game', layer: 2, visible: false }) } function random_shape() { var key = shapeKeys[floor(random.random() * length(shapeKeys))] return { type: key, color: SHAPES[key].color, blocks: array(SHAPES[key].blocks, function(b) { return [b[0], b[1]] }) } } function collides(px, py, blocks) { var i = 0, x = 0, y = 0 for (i = 0; i < length(blocks); i++) { x = px + blocks[i][0] y = py + blocks[i][1] if (x < 0 || x >= COLS || y < 0 || y >= ROWS) return true if (y >= 0 && board[y][x]) return true } return false } function lock_piece() { var i = 0, x = 0, y = 0 for (i = 0; i < length(piece.blocks); i++) { x = pieceX + piece.blocks[i][0] y = pieceY + piece.blocks[i][1] if (y >= 0 && y < ROWS && x >= 0 && x < COLS) { board[y][x] = piece.color board_shapes[y][x].fill = piece.color board_shapes[y][x].visible = true } } } function rotate_blocks(blocks) { var i = 0, x = 0, y = 0 for (i = 0; i < length(blocks); i++) { x = blocks[i][0] y = blocks[i][1] blocks[i][0] = y blocks[i][1] = -x } } function clear_lines() { var lines = 0 var r = ROWS - 1, c = 0, full = false, newRow = null, sr = 0 while (r >= 0) { full = true for (c = 0; c < COLS; c++) { if (!board[r][c]) { full = false; break } } if (full) { lines++ // Shift rows down sr = r while (sr > 0) { for (c = 0; c < COLS; c++) { board[sr][c] = board[sr - 1][c] if (board[sr][c]) { board_shapes[sr][c].fill = board[sr][c] board_shapes[sr][c].visible = true } else { board_shapes[sr][c].visible = false } } sr-- } for (c = 0; c < COLS; c++) { board[0][c] = null board_shapes[0][c].visible = false } } else { r-- } } if (lines == 1) score += 100 else if (lines == 2) score += 300 else if (lines == 3) score += 500 else if (lines == 4) score += 800 linesCleared += lines level = floor(linesCleared / 10) score_label.text = "Score: " + text(score) level_label.text = "Level: " + text(level) } function update_piece_shapes() { var i = 0, bx = 0, by = 0 for (i = 0; i < 4; i++) { bx = pieceX + piece.blocks[i][0] by = pieceY + piece.blocks[i][1] piece_shapes[i].pos.x = bx * TILE + TILE / 2 piece_shapes[i].pos.y = by * TILE + TILE / 2 piece_shapes[i].fill = piece.color piece_shapes[i].visible = true } } function update_next_display() { var i = 0, bx = 0, by = 0 for (i = 0; i < 4; i++) { bx = nextPiece.blocks[i][0] by = nextPiece.blocks[i][1] next_shapes[i].pos.x = (COLS + 1) * TILE + bx * TILE + TILE / 2 next_shapes[i].pos.y = 20 + by * TILE + TILE / 2 next_shapes[i].fill = nextPiece.color next_shapes[i].visible = true } } function spawn_piece() { var i = 0 piece = nextPiece || random_shape() nextPiece = random_shape() pieceX = 3 pieceY = 0 if (collides(pieceX, pieceY, piece.blocks)) { gameOver = true i = 0 for (i = 0; i < 4; i++) piece_shapes[i].visible = false return } update_piece_shapes() update_next_display() } function place_piece() { lock_piece() clear_lines() spawn_piece() } // Create 4 shapes for active piece (layer 1 = on top of board) var pi = 0 for (pi = 0; pi < 4; pi++) { piece_shapes[] = shape.rect({ pos: {x: 0, y: 0}, width: TILE - 1, height: TILE - 1, fill: {r: 1, g: 1, b: 1, a: 1}, plane: 'game', layer: 1, visible: false }) } init_board() spawn_piece() // Compositor var comp_config = { clear: {r: 0, g: 0, b: 0, a: 1}, planes: [ {name: 'game', camera: game_cam, resolution: {width: GW, height: GH}, presentation: 'letterbox'}, {name: 'hud', camera: hud_cam, resolution: {width: GW, height: GH}, presentation: 'stretch'} ] } core.start({ width: 640, height: 480, title: "Tetris", update: function(dt) { if (gameOver) return var down = input.player1().down() var test = null // Horizontal movement with DAS var wantLeft = down.left var wantRight = down.right var newDir = 0 if (wantLeft && !wantRight) newDir = -1 else if (wantRight && !wantLeft) newDir = 1 if (newDir != 0) { if (newDir != hDir || !hHeld) { // First press if (!collides(pieceX + newDir, pieceY, piece.blocks)) pieceX += newDir hMoveTimer = hDelay hDir = newDir hHeld = true } else { hMoveTimer -= dt if (hMoveTimer <= 0) { if (!collides(pieceX + newDir, pieceY, piece.blocks)) pieceX += newDir hMoveTimer = hRepeat } } } else { hDir = 0 hHeld = false hMoveTimer = 0 } // Rotate (once per press) if (down.rotate) { if (!rotateHeld) { rotateHeld = true test = array(piece.blocks, function(b) { return [b[0], b[1]] }) rotate_blocks(test) if (!collides(pieceX, pieceY, test)) piece.blocks = test } } else { rotateHeld = false } // Soft drop softDropping = down.soft_drop // Hard drop (once per press) if (down.hard_drop) { while (!collides(pieceX, pieceY + 1, piece.blocks)) pieceY++ place_piece() update_piece_shapes() return } // Gravity var fallSpeed = softDropping ? 10 : 1 gravityTimer += dt * fallSpeed var dropInterval = max(0.05, baseGravity - level * 0.05) if (gravityTimer >= dropInterval) { gravityTimer = 0 if (!collides(pieceX, pieceY + 1, piece.blocks)) { pieceY++ } else { place_piece() } } update_piece_shapes() }, render: function() { return compositor.execute(compositor.compile(comp_config)) } })