This commit is contained in:
2026-02-24 21:08:46 -06:00
parent f87854fca1
commit 3d87fdeb5f
23 changed files with 1551 additions and 1086 deletions

View File

@@ -1,5 +1,5 @@
return {
title:"Bunnymark",
width:1200,
height:600,
title: "Bunnymark",
width: 1200,
height: 600
}

View File

@@ -1,59 +1,124 @@
var draw = use('draw2d')
var render = use('render')
var graphics = use('graphics')
var sprite = use('sprite')
var geom = use('geometry')
var config = use('config')
var color = use('color')
var core = use('core')
var camera = use('camera')
var compositor = use('compositor')
var input = use('input')
var sprite_factory = use('sprite')
var text2d = use('text2d')
var random = use('random')
var bunnyTex = graphics.texture("bunny")
var GW = 1200, GH = 600
// We'll store our bunnies in an array of objects: { x, y, vx, vy }
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}})
input.configure({
action_map: {
spawn: ['mouse_button_left']
}
})
// Bunny tracking
var bunnies = []
var bunny_sprites = []
var BUNNY_W = 26, BUNNY_H = 37
// Start with some initial bunnies:
var i = 0;
for (i = 0; i < 100; i++) {
push(bunnies, {
x: random.random() * config.width,
y: random.random() * config.height,
// HUD
var count_label = text2d({
text: "Bunnies: 0", pos: {x: 10, y: GH - 25},
plane: 'hud', size: 16, color: {r: 1, g: 1, b: 1, a: 1}
})
var fps_label = text2d({
text: "FPS: 0", pos: {x: 10, y: GH - 45},
plane: 'hud', size: 16, color: {r: 1, g: 1, b: 1, a: 1}
})
var fps_timer = 0
var frame_count = 0
function add_bunny(x, y) {
var bunny = {
vx: (random.random() * 300) - 150,
vy: (random.random() * 300) - 150
}
var s = sprite_factory({
image: "bunny",
pos: {x: x, y: y},
width: BUNNY_W, height: BUNNY_H,
anchor_x: 0.5, anchor_y: 0.5,
plane: 'game'
})
push(bunnies, bunny)
push(bunny_sprites, s)
}
var update = function(dt) {
// If left mouse is down, spawn some more bunnies:
var mouse = input.mousestate()
var i = 0;
var b = null;
if (mouse.left)
for (i = 0; i < 50; i++) {
push(bunnies, {
x: mouse.x,
y: mouse.y,
vx: (random.random() * 300) - 150,
vy: (random.random() * 300) - 150
})
// Initial bunnies
var i = 0
for (i = 0; i < 100; i++) {
add_bunny(
random.random() * GW,
random.random() * GH
)
}
count_label.text = "Bunnies: " + length(bunnies)
// Mouse position tracking
var mouse_x = GW / 2, mouse_y = GH / 2
var comp_config = {
clear: {r: 0.2, g: 0.2, b: 0.3, 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: 1200, height: 600, title: "Bunnymark",
input: function(ev) {
if (ev.type == 'mouse_motion') {
mouse_x = ev.pos.x
mouse_y = ev.pos.y
}
},
update: function(dt) {
// Spawn bunnies while clicking
var down = input.player1().down()
if (down.spawn) {
var si = 0
for (si = 0; si < 50; si++) {
add_bunny(mouse_x, mouse_y)
}
count_label.text = "Bunnies: " + length(bunnies)
}
// Update bunny positions and bounce them inside the screen:
for (i = 0; i < length(bunnies); i++) {
b = bunnies[i]
b.x += b.vx * dt
b.y += b.vy * dt
// Update all bunnies
var b = null, s = null
for (i = 0; i < length(bunnies); i++) {
b = bunnies[i]
s = bunny_sprites[i]
// Bounce off left/right edges
if (b.x < 0) { b.x = 0; b.vx = -b.vx }
else if (b.x > config.width) { b.x = config.width; b.vx = -b.vx }
s.pos.x += b.vx * dt
s.pos.y += b.vy * dt
// Bounce off bottom/top edges
if (b.y < 0) { b.y = 0; b.vy = -b.vy }
else if (b.y > config.height) { b.y = config.height; b.vy = -b.vy }
if (s.pos.x < 0) { s.pos.x = 0; b.vx = -b.vx }
else if (s.pos.x > GW) { s.pos.x = GW; b.vx = -b.vx }
if (s.pos.y < 0) { s.pos.y = 0; b.vy = -b.vy }
else if (s.pos.y > GH) { s.pos.y = GH; b.vy = -b.vy }
}
// FPS counter
frame_count++
fps_timer += dt
if (fps_timer >= 1) {
fps_label.text = "FPS: " + frame_count
frame_count = 0
fps_timer -= 1
}
},
render: function() {
return compositor.execute(compositor.compile(comp_config))
}
}
var hud = function() {
draw.images(bunnyTex, bunnies)
}
})

View File

@@ -1,403 +1,232 @@
/* main.js runs the demo with your prototype-based grid */
var core = use('core')
var camera = use('camera')
var compositor = use('compositor')
var input = use('input')
var shape = use('shape2d')
var sprite_factory = use('sprite')
var text2d = use('text2d')
var json = use('json')
var draw2d = use('prosperon/draw2d')
var Grid = use('grid')
var MovementSystem = use('movement').MovementSystem
var startingPos = use('pieces').startingPosition
var rules = use('rules')
var blob = use('blob')
var S = 60
var GW = S * 8, GH = S * 8
/*──── import our pieces + systems ───────────────────────────────────*/
var Grid = use('grid'); // your new ctor
var MovementSystem = use('movement').MovementSystem;
var startingPos = use('pieces').startingPosition;
var rules = use('rules');
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}})
/*──── build board ───────────────────────────────────────────────────*/
var grid = Grid(8, 8);
grid.width = 8; // (the ctor didn't store them)
grid.height = 8;
var mover = MovementSystem(grid, rules);
startingPos(grid);
/*──── networking and game state ─────────────────────────────────────*/
var gameState = 'waiting'; // 'waiting', 'searching', 'server_waiting', 'connected'
var isServer = false;
var opponent = null;
var myColor = null; // 'white' or 'black'
var isMyTurn = false;
function updateTitle() {
var title = "Misty Chess - ";
if (gameState == 'waiting') {
title += "Press S to start server or J to join";
} else if (gameState == 'searching') {
title += "Searching for server...";
} else if (gameState == 'server_waiting') {
title += "Waiting for player to join...";
} else if (gameState == 'connected') {
if (myColor) {
title += (mover.turn == myColor ? "Your turn (" + myColor + ")" : "Opponent's turn (" + mover.turn + ")");
} else {
title += mover.turn + " turn";
}
}
log.console(title)
}
// Initialize title
updateTitle();
/*──── mouse → click-to-move ─────────────────────────────────────────*/
var selectPos = null;
var hoverPos = null;
var holdingPiece = false;
var opponentMousePos = null;
var opponentHoldingPiece = false;
var opponentSelectPos = null;
function handleMouseButtonDown(e) {
if (e.which != 0) return;
// Don't allow piece selection unless we have an opponent
if (gameState != 'connected' || !opponent) return;
var mx = e.mouse.x;
var my = e.mouse.y;
var c = [floor(mx / 60), floor(my / 60)];
if (!grid.inBounds(c)) return;
var cell = grid.at(c);
if (length(cell) && cell[0].colour == mover.turn) {
selectPos = c;
holdingPiece = true;
// Send pickup notification to opponent
if (opponent) {
send(opponent, {
type: 'piece_pickup',
pos: c
});
}
} else {
selectPos = null;
}
}
function handleMouseButtonUp(e) {
if (e.which != 0 || !holdingPiece || !selectPos) return;
// Don't allow moves unless we have an opponent and it's our turn
if (gameState != 'connected' || !opponent || !isMyTurn) {
holdingPiece = false;
return;
}
var mx = e.mouse.x;
var my = e.mouse.y;
var c = [floor(mx / 60), floor(my / 60)];
if (!grid.inBounds(c)) {
holdingPiece = false;
return;
}
if (mover.tryMove(grid.at(selectPos)[0], c)) {
log.console("Made move from", selectPos, "to", c);
// Send move to opponent
log.console("Sending move to opponent:", opponent);
send(opponent, {
type: 'move',
from: selectPos,
to: c
});
isMyTurn = false; // It's now opponent's turn
log.console("Move sent, now opponent's turn");
selectPos = null;
updateTitle();
}
holdingPiece = false;
// Send piece drop notification to opponent
if (opponent) {
send(opponent, {
type: 'piece_drop'
});
}
}
function handleMouseMotion(e) {
var mx = e.pos.x;
var my = e.pos.y;
var c = [floor(mx / 60), floor(my / 60)];
if (!grid.inBounds(c)) {
hoverPos = null;
return;
}
hoverPos = c;
// Send mouse position to opponent in real-time
if (opponent && gameState == 'connected') {
send(opponent, {
type: 'mouse_move',
pos: c,
holding: holdingPiece,
selectPos: selectPos
});
}
}
function handleKeyDown(e) {
// S key - start server
if (e.scancode == 22 && gameState == 'waiting') { // S key
startServer();
}
// J key - join server
else if (e.scancode == 13 && gameState == 'waiting') { // J key
joinServer();
}
}
/*──── drawing helpers ───────────────────────────────────────────────*/
/* ── constants ─────────────────────────────────────────────────── */
var S = 60; // square size in px
var light = [0.93,0.93,0.93,1];
var dark = [0.25,0.25,0.25,1];
var allowedColor = [1.0, 0.84, 0.0, 1.0]; // Gold for allowed moves
var myMouseColor = [0.0, 1.0, 0.0, 1.0]; // Green for my mouse
var opponentMouseColor = [1.0, 0.0, 0.0, 1.0]; // Red for opponent mouse
/* ── draw one 8×8 chess board ──────────────────────────────────── */
function drawBoard() {
var y = 0;
var x = 0;
var isMyHover = null;
var isOpponentHover = null;
var isValidMove = null;
var color = null;
for (y = 0; y < 8; ++y)
for (x = 0; x < 8; ++x) {
isMyHover = hoverPos && hoverPos[0] == x && hoverPos[1] == y;
isOpponentHover = opponentMousePos && opponentMousePos[0] == x && opponentMousePos[1] == y;
isValidMove = selectPos && holdingPiece && isValidMoveForTurn(selectPos, [x, y]);
color = ((x+y)&1) ? dark : light;
if (isValidMove) {
color = allowedColor; // Gold for allowed moves
} else if (isMyHover && !isOpponentHover) {
color = myMouseColor; // Green for my mouse
} else if (isOpponentHover) {
color = opponentMouseColor; // Red for opponent mouse
}
draw2d.rectangle(
{ x: x*S, y: y*S, width: S, height: S },
{ thickness: 0 },
{ color: color }
);
}
}
function isValidMoveForTurn(from, to) {
if (!grid.inBounds(to)) return false;
var piece = grid.at(from)[0];
if (!piece) return false;
// Check if the destination has a piece of the same color
var destCell = grid.at(to);
if (length(destCell) && destCell[0].colour == piece.colour) {
return false;
}
return rules.canMove(piece, from, to, grid);
}
/* ── draw every live piece ─────────────────────────────────────── */
function drawPieces() {
var piece = null;
var r = null;
var opponentPiece = null;
grid.each(function (p) {
if (p.captured) return;
// Skip drawing the piece being held (by me or opponent)
if (holdingPiece && selectPos &&
p.coord[0] == selectPos[0] &&
p.coord[1] == selectPos[1]) {
return;
}
// Skip drawing the piece being held by opponent
if (opponentHoldingPiece && opponentSelectPos &&
p.coord[0] == opponentSelectPos[0] &&
p.coord[1] == opponentSelectPos[1]) {
return;
}
var pr = { x: p.coord[0]*S, y: p.coord[1]*S,
width:S, height:S };
draw2d.image(p.sprite, pr);
});
// Draw the held piece at the mouse position if we're holding one
if (holdingPiece && selectPos && hoverPos) {
piece = grid.at(selectPos)[0];
if (piece) {
r = { x: hoverPos[0]*S, y: hoverPos[1]*S,
width:S, height:S };
draw2d.image(piece.sprite, r);
}
}
// Draw opponent's held piece if they're dragging one
if (opponentHoldingPiece && opponentSelectPos && opponentMousePos) {
opponentPiece = grid.at(opponentSelectPos)[0];
if (opponentPiece) {
r = { x: opponentMousePos[0]*S, y: opponentMousePos[1]*S,
width:S, height:S };
// Draw with slight transparency to show it's the opponent's piece
draw2d.image(opponentPiece.sprite, r);
}
}
}
function update(dt)
{
return {}
}
function draw()
{
draw2d.clear()
drawBoard()
drawPieces()
return draw2d.get_commands()
}
function startServer() {
gameState = 'server_waiting';
isServer = true;
myColor = 'white';
isMyTurn = true;
updateTitle();
$portal(e => {
log.console("Portal received contact message");
// Reply with this actor to establish connection
log.console ($self)
send(e, $self);
log.console("Portal replied with server actor");
}, 5678);
}
function joinServer() {
gameState = 'searching';
updateTitle();
function contact_fn(actor, reason) {
log.console("CONTACTED!", actor ? "SUCCESS" : "FAILED", reason);
if (actor) {
opponent = actor;
log.console("Connection established with server, sending join request");
// Send a greet message with our actor object
send(opponent, {
type: 'greet',
client_actor: $self
});
} else {
log.console(`Failed to connect: ${reason}`);
gameState = 'waiting';
updateTitle();
}
}
$contact(contact_fn, {
address: "192.168.0.149",
port: 5678
});
}
$receiver(e => {
var fromCell = null;
var piece = null;
if (e.kind == 'update')
send(e, update(e.dt))
else if (e.kind == 'draw')
send(e, draw())
else if (e.type == 'game_start' || e.type == 'move' || e.type == 'greet')
log.console("Receiver got message:", e.type, e);
if (e.type == 'greet') {
log.console("Server received greet from client");
// Store the client's actor object for ongoing communication
opponent = e.client_actor;
log.console("Stored client actor:", opponent);
gameState = 'connected';
updateTitle();
// Send game_start to the client
log.console("Sending game_start to client");
send(opponent, {
type: 'game_start',
your_color: 'black'
});
log.console("game_start message sent to client");
}
else if (e.type == 'game_start') {
log.console("Game starting, I am:", e.your_color);
myColor = e.your_color;
isMyTurn = (myColor == 'white');
gameState = 'connected';
updateTitle();
} else if (e.type == 'move') {
log.console("Received move from opponent:", e.from, "to", e.to);
// Apply opponent's move
fromCell = grid.at(e.from);
if (length(fromCell)) {
piece = fromCell[0];
if (mover.tryMove(piece, e.to)) {
isMyTurn = true; // It's now our turn
updateTitle();
log.console("Applied opponent move, now my turn");
} else {
log.console("Failed to apply opponent move");
}
} else {
log.console("No piece found at from position");
}
} else if (e.type == 'mouse_move') {
// Update opponent's mouse position
opponentMousePos = e.pos;
opponentHoldingPiece = e.holding;
opponentSelectPos = e.selectPos;
} else if (e.type == 'piece_pickup') {
// Opponent picked up a piece
opponentSelectPos = e.pos;
opponentHoldingPiece = true;
} else if (e.type == 'piece_drop') {
// Opponent dropped their piece
opponentHoldingPiece = false;
opponentSelectPos = null;
} else if (e.type == 'mouse_button_down') {
handleMouseButtonDown(e)
} else if (e.type == 'mouse_button_up') {
handleMouseButtonUp(e)
} else if (e.type == 'mouse_motion') {
handleMouseMotion(e)
} else if (e.type == 'key_down') {
handleKeyDown(e)
input.configure({
action_map: {
select: ['mouse_button_left'],
cancel: ['escape', 'mouse_button_right']
}
})
// Build board
var grid = Grid(8, 8)
grid.width = 8
grid.height = 8
var mover = MovementSystem(grid, rules)
startingPos(grid)
// Board squares (shape2d)
var light_color = {r: 0.93, g: 0.93, b: 0.85, a: 1}
var dark_color = {r: 0.45, g: 0.55, b: 0.35, a: 1}
var select_color = {r: 1, g: 0.84, b: 0, a: 1}
var valid_color = {r: 0.6, g: 0.8, b: 0.4, a: 1}
var board_shapes = []
var bx = 0, by = 0
for (by = 0; by < 8; by++) {
var row = []
for (bx = 0; bx < 8; bx++) {
var col = ((bx + by) & 1) ? dark_color : light_color
push(row, shape.rect({
pos: {x: bx * S + S / 2, y: by * S + S / 2},
width: S, height: S,
fill: {r: col.r, g: col.g, b: col.b, a: col.a},
plane: 'game', layer: 0
}))
}
push(board_shapes, row)
}
// Piece sprites — one per piece, keyed by piece object
var piece_sprites = {}
var piece_id = 0
grid.each(function(p) {
piece_id++
p._id = piece_id
piece_sprites[piece_id] = sprite_factory({
image: p.sprite,
pos: {x: p.coord[0] * S + S / 2, y: p.coord[1] * S + S / 2},
width: S, height: S,
anchor_x: 0.5, anchor_y: 0.5,
plane: 'game', layer: 1
})
})
// Selection state
var selectPos = null
var validMoves = []
// Mouse position in grid coords
var hover_gx = -1, hover_gy = -1
// Status text
var status_label = text2d({
text: "White's turn", pos: {x: 10, y: GH - 20},
plane: 'hud', size: 14, color: {r: 1, g: 1, b: 1, a: 1}
})
function update_status() {
status_label.text = mover.turn + "'s turn"
}
function reset_board_colors() {
var x = 0, y = 0
for (y = 0; y < 8; y++) {
for (x = 0; x < 8; x++) {
var col = ((x + y) & 1) ? dark_color : light_color
board_shapes[y][x].fill = {r: col.r, g: col.g, b: col.b, a: col.a}
}
}
}
function highlight_selection() {
reset_board_colors()
if (!selectPos) return
// Highlight selected square
board_shapes[selectPos[1]][selectPos[0]].fill = {
r: select_color.r, g: select_color.g, b: select_color.b, a: select_color.a
}
// Highlight valid moves
var i = 0
for (i = 0; i < length(validMoves); i++) {
var m = validMoves[i]
board_shapes[m[1]][m[0]].fill = {
r: valid_color.r, g: valid_color.g, b: valid_color.b, a: valid_color.a
}
}
}
function compute_valid_moves(from) {
validMoves = []
var piece = grid.at(from)[0]
if (!piece) return
var x = 0, y = 0, to = null, dest = null
for (y = 0; y < 8; y++) {
for (x = 0; x < 8; x++) {
to = [x, y]
dest = grid.at(to)
if (length(dest) && dest[0].colour == piece.colour) continue
if (rules.canMove(piece, from, to, grid))
push(validMoves, to)
}
}
}
function sync_piece_sprites() {
grid.each(function(p) {
var spr = piece_sprites[p._id]
if (!spr) return
if (p.captured) {
spr.visible = false
} else {
spr.pos.x = p.coord[0] * S + S / 2
spr.pos.y = p.coord[1] * S + S / 2
spr.visible = true
}
})
}
// Input handler
var game_input = {
on_input: function(action, data) {
if (!data.pressed) return
if (action == 'cancel') {
selectPos = null
validMoves = []
highlight_selection()
return
}
if (action == 'select' && hover_gx >= 0 && hover_gx < 8 && hover_gy >= 0 && hover_gy < 8) {
var clicked = [hover_gx, hover_gy]
var cell = grid.at(clicked)
if (selectPos) {
// Try to move
var is_valid = false
var i = 0
for (i = 0; i < length(validMoves); i++) {
if (validMoves[i][0] == clicked[0] && validMoves[i][1] == clicked[1]) {
is_valid = true
break
}
}
if (is_valid) {
var src_piece = grid.at(selectPos)[0]
if (src_piece && mover.tryMove(src_piece, clicked)) {
sync_piece_sprites()
update_status()
}
selectPos = null
validMoves = []
} else if (length(cell) && cell[0].colour == mover.turn) {
// Select different piece
selectPos = clicked
compute_valid_moves(selectPos)
} else {
selectPos = null
validMoves = []
}
} else {
// Select piece
if (length(cell) && cell[0].colour == mover.turn) {
selectPos = clicked
compute_valid_moves(selectPos)
}
}
highlight_selection()
}
}
}
input.player1().possess(game_input)
var comp_config = {
clear: {r: 0.15, g: 0.15, b: 0.2, 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: 640, title: "Chess",
input: function(ev) {
if (ev.type == 'mouse_motion') {
// Convert pixel coords to grid coords via camera
var wp = game_cam.window_to_world(ev.pos.x, ev.pos.y)
if (wp) {
hover_gx = floor(wp.x / S)
hover_gy = floor(wp.y / S)
}
}
},
update: function(dt) {
},
render: function() {
return compositor.execute(compositor.compile(comp_config))
}
})

View File

@@ -1,9 +1,5 @@
// Chess game configuration for Moth framework
return {
title: "Chess",
resolution: { width: 480, height: 480 },
internal_resolution: { width: 480, height: 480 },
fps: 60,
clearColor: [22/255, 120/255, 194/255, 1],
mode: 'stretch' // No letterboxing for chess
};
width: 640,
height: 640
}

View File

@@ -1,5 +1,5 @@
return {
title: "Pong",
width: 858,
height: 525
width: 960,
height: 540
}

View File

@@ -1,86 +1,127 @@
// main.js
var draw = use('draw2d')
var config = use('config')
var color = use('color')
var random = use('random')
var core = use('core')
var camera = use('camera')
var compositor = use('compositor')
var input = use('input')
var shape = use('shape2d')
var text2d = use('text2d')
prosperon.camera.transform.pos = [0,0]
var paddleW = 10, paddleH = 80
var p1 = {x: 30, y: config.height*0.5, speed: 300}
var p2 = {x: config.width-30, y: config.height*0.5, speed: 300}
var ball = {x: 0, y: 0, vx: 220, vy: 150, size: 10}
var GW = 480, GH = 270
var paddleW = 8, paddleH = 50, speed = 200
var ballSize = 8
var bvx = 220, bvy = 150
var score1 = 0, score2 = 0
function resetBall() {
ball.x = config.width*0.5
ball.y = config.height*0.5
// give it a random vertical bounce
ball.vy = (random.random()<0.5 ? -1:1)*150
// keep horizontal speed to the same magnitude
ball.vx = ball.vx>0 ? 220 : -220
// Cameras: game at pixel-art res, HUD at native
var game_cam = camera.make({width: GW, height: GH, pos: {x: GW / 2, y: GH / 2}})
var hud_cam = camera.make({width: 960, height: 540, pos: {x: 480, y: 270}})
// Action mapping
input.configure({
action_map: {
p1_up: ['w'],
p1_down: ['s'],
p2_up: ['up'],
p2_down: ['down']
}
})
// Retained shapes — game plane
var midline = shape.rect({
pos: {x: GW / 2, y: GH / 2}, width: 2, height: GH,
fill: {r: 0.3, g: 0.3, b: 0.3, a: 1}, plane: 'game'
})
var p1 = shape.rect({
pos: {x: 20, y: GH / 2}, width: paddleW, height: paddleH,
fill: {r: 1, g: 1, b: 1, a: 1}, plane: 'game'
})
var p2 = shape.rect({
pos: {x: GW - 20, y: GH / 2}, width: paddleW, height: paddleH,
fill: {r: 1, g: 1, b: 1, a: 1}, plane: 'game'
})
var ball = shape.rect({
pos: {x: GW / 2, y: GH / 2}, width: ballSize, height: ballSize,
fill: {r: 1, g: 1, b: 1, a: 1}, plane: 'game', groups: ['glow']
})
// HUD plane — score text
var score_label = text2d({
text: "0 0", pos: {x: 420, y: 490},
plane: 'hud', size: 32, color: {r: 1, g: 1, b: 1, a: 1}
})
function reset_ball() {
ball.pos.x = GW / 2
ball.pos.y = GH / 2
bvy = (bvy > 0 ? -1 : 1) * 150
bvx = bvx > 0 ? -220 : 220
}
resetBall()
var update = function(dt) {
// Move paddles: positive Y is up, so W/↑ means p.y += speed
if (input.keyboard.down('w')) p1.y += p1.speed*dt
if (input.keyboard.down('s')) p1.y -= p1.speed*dt
// Paddle 2 movement (ArrowUp = up, ArrowDown = down)
if (input.keyboard.down('i')) p2.y += p2.speed*dt
if (input.keyboard.down('k')) p2.y -= p2.speed*dt
// Clamp paddles to screen
if (p1.y < paddleH*0.5) p1.y = paddleH*0.5
if (p1.y > config.height - paddleH*0.5) p1.y = config.height - paddleH*0.5
if (p2.y < paddleH*0.5) p2.y = paddleH*0.5
if (p2.y > config.height - paddleH*0.5) p2.y = config.height - paddleH*0.5
// Move ball
ball.x += ball.vx*dt
ball.y += ball.vy*dt
// Bounce top/bottom
if (ball.y+ball.size*0.5>config.height || ball.y-ball.size*0.5<0) ball.vy = -ball.vy
// Check paddle collisions
// p1 bounding box
var left1 = p1.x - paddleW*0.5, right1 = p1.x + paddleW*0.5
var top1 = p1.y + paddleH*0.5, bottom1 = p1.y - paddleH*0.5
// p2 bounding box
var left2 = p2.x - paddleW*0.5, right2 = p2.x + paddleW*0.5
var top2 = p2.y + paddleH*0.5, bottom2 = p2.y - paddleH*0.5
// ball half-edges
var l = ball.x - ball.size*0.5, r = ball.x + ball.size*0.5
var b = ball.y - ball.size*0.5, t = ball.y + ball.size*0.5
// Collide with paddle 1?
if (r>left1 && l<right1 && t>bottom1 && b<top1)
ball.vx = abs(ball.vx)
// Collide with paddle 2?
if (r>left2 && l<right2 && t>bottom2 && b<top2)
ball.vx = -abs(ball.vx)
// Check left/right out-of-bounds
if (r<0) { score2++; resetBall() }
if (l>config.width) { score1++; resetBall() }
// Compositor: game plane with bloom on ball, HUD overlay
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: 960, height: 540}, presentation: 'stretch'}
],
group_effects: {
glow: {effects: [{type: 'bloom', threshold: 0.3, intensity: 2}]}
}
}
var hud = function() {
// Clear screen black
draw.rectangle({x:0, y:0, width:config.width, height:config.height}, [0,0,0,1])
core.start({
width: 960, height: 540, title: "Pong",
// Draw paddles
draw.rectangle({x:p1.x - paddleW*0.5, y:p1.y - paddleH*0.5, width:paddleW, height:paddleH}, color.white)
draw.rectangle({x:p2.x - paddleW*0.5, y:p2.y - paddleH*0.5, width:paddleW, height:paddleH}, color.white)
update: function(dt) {
var down = input.player1().down()
// Draw ball
draw.rectangle({x:ball.x - ball.size*0.5, y:ball.y - ball.size*0.5, width:ball.size, height:ball.size}, color.white)
// Move paddles
if (down.p1_up) p1.pos.y += speed * dt
if (down.p1_down) p1.pos.y -= speed * dt
if (down.p2_up) p2.pos.y += speed * dt
if (down.p2_down) p2.pos.y -= speed * dt
// Simple score display
var msg = score1 + " " + score2
draw.text(msg, {x:0, y:10, width:config.width, height:40}, null, 0, color.white, 0)
}
// Clamp paddles
var hh = paddleH / 2
if (p1.pos.y < hh) p1.pos.y = hh
if (p1.pos.y > GH - hh) p1.pos.y = GH - hh
if (p2.pos.y < hh) p2.pos.y = hh
if (p2.pos.y > GH - hh) p2.pos.y = GH - hh
// Move ball
ball.pos.x += bvx * dt
ball.pos.y += bvy * dt
// Bounce top/bottom
var bs = ballSize / 2
if (ball.pos.y - bs < 0 || ball.pos.y + bs > GH) bvy = -bvy
// Paddle collisions
var bx = ball.pos.x, by = ball.pos.y
var pw = paddleW / 2, ph = paddleH / 2
if (bx - bs < p1.pos.x + pw && bx + bs > p1.pos.x - pw &&
by + bs > p1.pos.y - ph && by - bs < p1.pos.y + ph)
bvx = abs(bvx)
if (bx + bs > p2.pos.x - pw && bx - bs < p2.pos.x + pw &&
by + bs > p2.pos.y - ph && by - bs < p2.pos.y + ph)
bvx = -abs(bvx)
// Scoring
if (bx < 0) {
score2++
score_label.text = score1 + " " + score2
reset_ball()
}
if (bx > GW) {
score1++
score_label.text = score1 + " " + score2
reset_ball()
}
},
render: function() {
return compositor.execute(compositor.compile(comp_config))
}
})

View File

@@ -1,125 +1,192 @@
// main.js
var draw = use('draw2d')
var render = use('render')
var graphics = use('graphics')
var core = use('core')
var camera = use('camera')
var compositor = use('compositor')
var input = use('input')
var config = use('config')
var color = use('color')
var shape = use('shape2d')
var text2d = use('text2d')
var tw = use('tween')
var ease = use('ease')
var random = use('random')
prosperon.camera.transform.pos = [0,0]
var GW = 600, GH = 600
var cellSize = 20
var gridW = floor(config.width / cellSize)
var gridH = floor(config.height / cellSize)
var gridW = GW / cellSize
var gridH = GH / cellSize
var snake = null, direction = null, nextDirection = null, apple = null
var moveInterval = 0.1
var moveTimer = 0
var gameState = "playing"
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}})
function resetGame() {
var cx = floor(gridW / 2)
var cy = floor(gridH / 2)
snake = [
{x: cx, y: cy},
{x: cx-1, y: cy},
{x: cx-2, y: cy}
]
direction = {x:1, y:0}
nextDirection = {x:1, y:0}
spawnApple()
gameState = "playing"
moveTimer = 0
// Action mapping with possession
input.configure({
action_map: {
up: ['w', 'up'],
down: ['s', 'down'],
left: ['a', 'left'],
right: ['d', 'right'],
restart: ['space']
}
})
// Game state
var snake_shapes = []
var snake_pos = []
var dir = {x: 1, y: 0}
var next_dir = {x: 1, y: 0}
var move_timer = 0
var move_interval = 0.12
var state = 'playing'
var gameover_label = null
var score = 0
var score_label = null
// Input handler entity — possessed by player1
var game_input = {
on_input: function(action, data) {
if (!data.pressed) return
if (state == 'playing') {
if (action == 'up' && dir.y != -1) next_dir = {x: 0, y: 1}
if (action == 'down' && dir.y != 1) next_dir = {x: 0, y: -1}
if (action == 'left' && dir.x != 1) next_dir = {x: -1, y: 0}
if (action == 'right' && dir.x != -1) next_dir = {x: 1, y: 0}
}
if (action == 'restart' && state == 'gameover') reset_game()
}
}
input.player1().possess(game_input)
function grid_to_world(gx, gy) {
return {x: gx * cellSize + cellSize / 2, y: gy * cellSize + cellSize / 2}
}
function spawnApple() {
apple = {x:floor(random.random()*gridW), y:floor(random.random()*gridH)}
// Re-spawn if apple lands on snake
var i = 0;
for (i=0; i<length(snake); i++)
if (snake[i].x == apple.x && snake[i].y == apple.y) { spawnApple(); return }
}
// Apple
var apple_gx = 0, apple_gy = 0
var apple_shape = shape.rect({
pos: {x: 0, y: 0}, width: cellSize - 2, height: cellSize - 2,
fill: {r: 1, g: 0.2, b: 0.2, a: 1}, plane: 'game'
})
function wrap(pos) {
if (pos.x < 0) pos.x = gridW - 1
if (pos.x >= gridW) pos.x = 0
if (pos.y < 0) pos.y = gridH - 1
if (pos.y >= gridH) pos.y = 0
}
resetGame()
var update = function(dt) {
if (gameState != "playing") return
moveTimer += dt
if (moveTimer < moveInterval) return
moveTimer -= moveInterval
// Update direction
direction = {x: nextDirection.x, y: nextDirection.y}
// New head
var head = {x: snake[0].x + direction.x, y: snake[0].y + direction.y}
wrap(head)
// Check collision with body
var i = 0;
for (i=0; i<length(snake); i++) {
if (snake[i].x == head.x && snake[i].y == head.y) {
gameState = "gameover"
function spawn_apple() {
apple_gx = floor(random.random() * gridW)
apple_gy = floor(random.random() * gridH)
var i = 0
for (i = 0; i < length(snake_pos); i++) {
if (snake_pos[i].x == apple_gx && snake_pos[i].y == apple_gy) {
spawn_apple()
return
}
}
// Place head
snake.unshift(head)
// Eat apple?
if (head.x == apple.x && head.y == apple.y) spawnApple()
else pop(snake)
var wp = grid_to_world(apple_gx, apple_gy)
apple_shape.pos.x = wp.x
apple_shape.pos.y = wp.y
}
var hud = function() {
// Optional clear screen
draw.rectangle({x:0, y:0, width:config.width, height:config.height}, [0,0,0,1])
function reset_game() {
var i = 0
for (i = 0; i < length(snake_shapes); i++)
snake_shapes[i].destroy()
snake_shapes = []
snake_pos = []
// Draw snake
var i = 0;
var s = null;
var msg = null;
for (i=0; i<length(snake); i++) {
s = snake[i]
draw.rectangle({x:s.x*cellSize, y:s.y*cellSize, width:cellSize, height:cellSize}, color.green)
var cx = floor(gridW / 2)
var cy = floor(gridH / 2)
var wp = null
for (i = 0; i < 3; i++) {
push(snake_pos, {x: cx - i, y: cy})
wp = grid_to_world(cx - i, cy)
push(snake_shapes, shape.rect({
pos: {x: wp.x, y: wp.y}, width: cellSize - 2, height: cellSize - 2,
fill: {r: 0, g: 1, b: 0.3, a: 1}, plane: 'game'
}))
}
// Draw apple
draw.rectangle({x:apple.x*cellSize, y:apple.y*cellSize, width:cellSize, height:cellSize}, color.red)
if (gameState == "gameover") {
msg = "GAME OVER! Press SPACE to restart."
draw.text(msg, {x:0, y:config.height*0.5-10, width:config.width, height:20}, null, 0, color.white)
}
dir = {x: 1, y: 0}
next_dir = {x: 1, y: 0}
state = 'playing'
move_timer = 0
score = 0
if (score_label) score_label.text = "Score: 0"
if (gameover_label) { gameover_label.destroy(); gameover_label = null }
spawn_apple()
}
// No immediate reversal
// "Up" means y=1, so going physically up on screen
var inputs = {
up: function() {
if (direction.y != -1) nextDirection = {x:0,y:1}
},
down: function() {
if (direction.y != 1) nextDirection = {x:0,y:-1}
},
left: function() {
if (direction.x != 1) nextDirection = {x:-1,y:0}
},
right: function() {
if (direction.x != -1) nextDirection = {x:1,y:0}
},
space: function() {
if (gameState=="gameover") resetGame()
}
// HUD
score_label = text2d({
text: "Score: 0", pos: {x: 10, y: GH - 30},
plane: 'hud', size: 16, color: {r: 1, g: 1, b: 1, a: 1}
})
reset_game()
var comp_config = {
clear: {r: 0.08, g: 0.08, b: 0.1, 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'}
]
}
//input.player[0].control()
core.start({
width: 600, height: 600, title: "Snake",
update: function(dt) {
if (state != 'playing') return
move_timer += dt
if (move_timer < move_interval) return
move_timer -= move_interval
dir.x = next_dir.x
dir.y = next_dir.y
// New head position
var hx = snake_pos[0].x + dir.x
var hy = snake_pos[0].y + dir.y
// Wrap
if (hx < 0) hx = gridW - 1
if (hx >= gridW) hx = 0
if (hy < 0) hy = gridH - 1
if (hy >= gridH) hy = 0
// Self collision
var i = 0
for (i = 0; i < length(snake_pos); i++) {
if (snake_pos[i].x == hx && snake_pos[i].y == hy) {
state = 'gameover'
gameover_label = text2d({
text: "GAME OVER — SPACE to restart",
pos: {x: GW / 2 - 170, y: GH / 2},
plane: 'hud', size: 20, color: {r: 1, g: 0.3, b: 0.3, a: 1}
})
return
}
}
// Add head with tween from old head position
var old_wp = grid_to_world(snake_pos[0].x, snake_pos[0].y)
var new_wp = grid_to_world(hx, hy)
snake_pos.unshift({x: hx, y: hy})
var head = shape.rect({
pos: {x: old_wp.x, y: old_wp.y}, width: cellSize - 2, height: cellSize - 2,
fill: {r: 0, g: 1, b: 0.3, a: 1}, plane: 'game'
})
// Smooth tween from old position to new position
tw.tween(head.pos).to({x: new_wp.x, y: new_wp.y}, move_interval).ease(ease.linear)
snake_shapes.unshift(head)
// Eat apple?
if (hx == apple_gx && hy == apple_gy) {
score++
score_label.text = "Score: " + score
spawn_apple()
} else {
// Remove tail
var tail = pop(snake_shapes)
tail.destroy()
pop(snake_pos)
}
},
render: function() {
return compositor.execute(compositor.compile(comp_config))
}
})

View File

@@ -1,123 +1,151 @@
var draw = use('draw2d')
var core = use('core')
var camera = use('camera')
var compositor = use('compositor')
var input = use('input')
var config = use('config')
var color = use('color')
var shape = use('shape2d')
var text2d = use('text2d')
var random = use('random')
prosperon.camera.transform.pos = [0,0]
// Board constants
var COLS = 10, ROWS = 20
var TILE = 6 // each cell is 6x6
var TILE = 8
var GW = COLS * TILE + 80 // extra space for next piece + score
var GH = ROWS * TILE
// Board storage (2D), each cell is either 0 or a [r,g,b,a] color
var board = []
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}})
// Gravity timing
var baseGravity = 0.8 // seconds between drops at level 0
var gravityTimer = 0
// Current piece & position
var piece = null
var pieceX = 0
var pieceY = 0
// Next piece
var nextPiece = null
// Score/lines/level
var score = 0
var linesCleared = 0
var level = 0
// Rotation lock to prevent spinning with W
var rotateHeld = false
var gameOver = false
// Horizontal movement gating
var hMoveTimer = 0
var hDelay = 0.2 // delay before repeated moves begin
var hRepeat = 0.05 // time between repeated moves
var prevLeft = false
var prevRight = false
// 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:[0,1,1,1], blocks:[[0,0],[1,0],[2,0],[3,0]] },
O: { color:[1,1,0,1], blocks:[[0,0],[1,0],[0,1],[1,1]] },
T: { color:[1,0,1,1], blocks:[[0,0],[1,0],[2,0],[1,1]] },
S: { color:[0,1,0,1], blocks:[[1,0],[2,0],[0,1],[1,1]] },
Z: { color:[1,0,0,1], blocks:[[0,0],[1,0],[1,1],[2,1]] },
J: { color:[0,0,1,1], blocks:[[0,0],[0,1],[1,1],[2,1]] },
L: { color:[1,0.5,0,1], blocks:[[2,0],[0,1],[1,1],[2,1]] }
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)
// Initialize board with empty (0)
function initBoard() {
// Board: 2D array, null or color object
var board = []
// Board shapes: one shape per cell, toggled visible
var board_shapes = []
function init_board() {
board = []
var r = 0;
var row = null;
var c = 0;
for (r=0; r<ROWS; r++) {
row = []
for (c=0; c<COLS; c++) push(row, 0)
board_shapes = []
var r = 0, c = 0
for (r = 0; r < ROWS; r++) {
var row = []
var srow = []
for (c = 0; c < COLS; c++) {
push(row, null)
push(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
}))
}
push(board, row)
push(board_shapes, srow)
}
}
initBoard()
function randomShape() {
var key = shapeKeys[floor(random.random()*length(shapeKeys))]
// Make a copy of the shape's blocks
// 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++) {
push(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, b => [b[0], b[1]])
blocks: array(SHAPES[key].blocks, function(b) { return [b[0], b[1]] })
}
}
function spawnPiece() {
piece = nextPiece || randomShape()
nextPiece = randomShape()
pieceX = 3
pieceY = 0
// Collision on spawn => game over
if (collides(pieceX, pieceY, piece.blocks)) gameOver = true
}
function collides(px, py, blocks) {
var i = 0;
var x = 0;
var y = 0;
for (i=0; i<length(blocks); i++) {
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
if (x < 0 || x >= COLS || y < 0 || y >= ROWS) return true
if (y >= 0 && board[y][x]) return true
}
return false
}
// Lock piece into board
function lockPiece() {
var i = 0;
var x = 0;
var y = 0;
for (i=0; i<length(piece.blocks); i++) {
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) board[y][x] = piece.color
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
}
}
}
// Rotate 90° clockwise
function rotate(blocks) {
// (x,y) => (y,-x)
var i = 0;
var x = 0;
var y = 0;
for (i=0; i<length(blocks); i++) {
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
@@ -125,173 +153,190 @@ function rotate(blocks) {
}
}
function clearLines() {
function clear_lines() {
var lines = 0
var r = ROWS-1;
var newRow = null;
var c = 0;
for (r=ROWS-1; r>=0;) {
if (every(board[r], cell => cell)) {
var r = ROWS - 1, c = 0, full = false, newRow = null
while (r >= 0) {
full = true
for (c = 0; c < COLS; c++) {
if (!board[r][c]) { full = false; break }
}
if (full) {
lines++
// remove row
board = array(array(board, 0, r), array(board, r+1))
// add empty row on top
newRow = []
for (c=0; c<COLS; c++) push(newRow, 0)
board.unshift(newRow)
// Shift rows down
var 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--
}
}
// Score
if (lines==1) score += 100
else if (lines==2) score += 300
else if (lines==3) score += 500
else if (lines==4) score += 800
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)
level = floor(linesCleared / 10)
score_label.text = "Score: " + score
level_label.text = "Level: " + level
}
function placePiece() {
lockPiece()
clearLines()
spawnPiece()
function update_piece_shapes() {
var i = 0
for (i = 0; i < 4; i++) {
var bx = pieceX + piece.blocks[i][0]
var 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
}
}
// Hard drop
function hardDrop() {
while(!collides(pieceX, pieceY+1, piece.blocks)) pieceY++
placePiece()
function update_next_display() {
var i = 0
for (i = 0; i < 4; i++) {
var bx = nextPiece.blocks[i][0]
var 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
}
}
spawnPiece()
var update = function(dt) {
if (gameOver) return
// ======= Horizontal Movement Gate =======
var leftPressed = input.keyboard.down('a')
var rightPressed = input.keyboard.down('d')
var horizontalMove = 0
// If user just pressed A, move once & start gating
if (leftPressed && !prevLeft) {
horizontalMove = -1
hMoveTimer = hDelay
}
// If user is holding A & the timer is up, move again, then reset timer to repeat
else if (leftPressed && hMoveTimer <= 0) {
horizontalMove = -1
hMoveTimer = hRepeat
function spawn_piece() {
piece = nextPiece || random_shape()
nextPiece = random_shape()
pieceX = 3
pieceY = 0
if (collides(pieceX, pieceY, piece.blocks)) {
gameOver = true
var i = 0
for (i = 0; i < 4; i++) piece_shapes[i].visible = false
return
}
update_piece_shapes()
update_next_display()
}
// Same logic for D
if (rightPressed && !prevRight) {
horizontalMove = 1
hMoveTimer = hDelay
} else if (rightPressed && hMoveTimer <= 0) {
horizontalMove = 1
hMoveTimer = hRepeat
}
function place_piece() {
lock_piece()
clear_lines()
spawn_piece()
}
// Move horizontally if it doesn't collide
if (horizontalMove < 0 && !collides(pieceX-1, pieceY, piece.blocks)) pieceX--
else if (horizontalMove > 0 && !collides(pieceX+1, pieceY, piece.blocks)) pieceX++
// Create 4 shapes for active piece (layer 1 = on top of board)
var pi = 0
for (pi = 0; pi < 4; pi++) {
push(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
}))
}
// If neither A nor D is pressed, reset the timer so next press is immediate
if (!leftPressed && !rightPressed) {
hMoveTimer = 0
}
init_board()
spawn_piece()
// Decrement horizontal timer
hMoveTimer -= dt
prevLeft = leftPressed
prevRight = rightPressed
// ======= End Horizontal Movement Gate =======
// 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'}
]
}
// Rotate with W (once per press, no spinning)
var test = null;
if (input.keyboard.down('w')) {
if (!rotateHeld) {
rotateHeld = true
test = array(piece.blocks, b => [b[0], b[1]])
rotate(test)
if (!collides(pieceX, pieceY, test)) piece.blocks = test
}
} else {
rotateHeld = false
}
core.start({
width: 640, height: 480, title: "Tetris",
// Soft drop if S is held (accelerates gravity)
var fallSpeed = input.keyboard.down('s') ? 10 : 1
update: function(dt) {
if (gameOver) return
// Gravity
gravityTimer += dt * fallSpeed
var dropInterval = max(0.1, baseGravity - level*0.05)
if (gravityTimer >= dropInterval) {
gravityTimer = 0
if (!collides(pieceX, pieceY+1, piece.blocks)) {
pieceY++
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 {
placePiece()
hDir = 0
hHeld = false
hMoveTimer = 0
}
}
// Hard drop if space is held
if (input.keyboard.down('space')) {
// hardDrop()
}
}
var hud = function() {
// Clear screen
draw.rectangle({x:0, y:0, width:config.width, height:config.height}, [0,0,0,1])
// Draw board
var r = 0;
var c = 0;
var cell = null;
var i = 0;
var x = 0;
var y = 0;
var nx = 0;
var ny = 0;
var dx = 0;
var dy = 0;
for (r=0; r<ROWS; r++) {
for (c=0; c<COLS; c++) {
cell = board[r][c]
if (!cell) continue
draw.rectangle({x:c*TILE, y:(ROWS-1-r)*TILE, width:TILE, height:TILE}, cell)
// 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
}
}
// Draw falling piece
if (!gameOver && piece) {
for (i=0; i<length(piece.blocks); i++) {
x = pieceX + piece.blocks[i][0]
y = pieceY + piece.blocks[i][1]
draw.rectangle({x:x*TILE, y:(ROWS-1-y)*TILE, width:TILE, height:TILE}, piece.color)
// 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
}
}
// Next piece window
draw.text("Next", {x:70, y:5, width:50, height:10}, null, 0, color.white)
if (nextPiece) {
for (i=0; i<length(nextPiece.blocks); i++) {
nx = nextPiece.blocks[i][0]
ny = nextPiece.blocks[i][1]
dx = 12 + nx
dy = 16 - ny
draw.rectangle({x:dx*TILE, y:(ROWS-1-dy)*TILE, width:TILE, height:TILE}, nextPiece.color)
// 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()
}
}
}
// Score & Level
var info = "Score: " + score + "\nLines: " + linesCleared + "\nLevel: " + level
draw.text(info, {x:70, y:30, width:90, height:50}, null, 0, color.white)
update_piece_shapes()
},
if (gameOver) {
draw.text("GAME OVER", {x:10, y:config.height*0.5-5, width:config.width-20, height:20}, null, 0, color.red)
render: function() {
return compositor.execute(compositor.compile(comp_config))
}
}
})