344 lines
8.6 KiB
Plaintext
344 lines
8.6 KiB
Plaintext
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))
|
|
}
|
|
})
|