Files
prosperon/examples/tetris/main.ce
2026-02-26 08:13:27 -06:00

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))
}
})