Towers walkthrough
This walkthrough explains how a Box2D match steers falling bricks and turns them into loose parts of a tower. Follow its physics, input, lobby and presentation, with code excerpts and the match and lobby sources.
Four plinths side by side, bricks that come down one at a time, and a race to build the tallest tower, with Box2D under all of it. A game made with Tide to show its 2D physics in a match: a brick that has landed is a body like any other, so towers lean, slide and come down, the same on every machine, whatever their pings. Play it alone against three towers that build themselves, or with friends in a room. The idea is Tricky Towers', the party game of stacking falling bricks.
Open it on a page of its own, which is the one to send to friends: see Play with friends.
Play
The game opens on its lobby, with all four towers building by themselves behind it. Play starts a game of your own against three of them, Host a room opens one for friends, and Join a room takes a friend's code.
| Action | Keyboard | Mouse or finger |
|---|---|---|
| Move the brick | Left and right, or A and D |
Drag sideways |
| Turn it | Up, or W |
Tap |
| Drop it | Hold down, S or Space |
Swipe down and hold, or press and hold still |
| Use what you earned | E |
The button at the bottom |
| The menu | Menu, or Escape | Menu |
Your tower is the one on the plinth that says YOU, your brick is the one with the white edge, and the lanes under it run down to its outline, where it will land. The row along the top says how high every tower stands, and the bar down the left side shows the race: a mark for each tower, yours the big one, and the finish line at the top.
- A brick falls where you steer it, half a cell at a time, and turns by quarters, wherever there's room for it. Where it touches down it's a real brick: it has weight, it leans and it slides. Then the next one comes. NEXT shows which.
- The first tower to stand over the finish line for three seconds takes the round. The line is 40 meters up. Everything then comes down at once, and the next round starts from nothing. Rounds won are the dots under a plinth's name.
- A brick that falls off your tower costs you time: your next brick waits longer, up to three seconds when a lot of them go at once. What fell lies where it landed for ten seconds, and is cleared.
- Every brick of a tower is loose, from the plinth up: nothing holds a tower together but how it was built. One that leans low down leans all the way up, and can go from anywhere.
- A tower earns something at 10, 20 and 30 meters, which the dotted lines across your plinth mark. A gust shoves the top of the tallest of the other towers away from yours. Cement sets every brick of your tower solid: they turn gray, and won't move again this round, whatever lands on them or shakes them. What you build on top of them is loose again. A quake shakes every plinth but yours. You hold one at a time, so use it before the next line.
- A round that goes on long ends anyway: after a minute and a half the finish line starts coming down, a meter every four seconds, until a tower is over it.
- Towers nobody steers build by themselves, each a little more or less careful than the next, and use what they earn. The menu has a button to stop them.
The bottom right corner shows the frame rate, and your ping in someone else's room. Click or tap it, or press F8, for the rest of what the engine is doing: the tick, the entities, what goes over the network and the systems that take longest. That's the game's stats setting.
In this page, click the game to give it the keyboard. On a touchscreen, a swipe on it scrolls the page until you tap the game, and again once you tap the page somewhere else. The game on a page of its own has every touch from the start.
Play with friends
A room takes four players, one for each plinth, on the web and on desktop alike. They meet by the room's code, and nobody waits for anybody: whoever arrives is in the race, on the next free plinth.
- Host. On the lobby, press Host a room. The room's code shows at once, like
K7QF2M, with the four seats under it and who has each. Copy puts the code on the clipboard, and Play takes you to your tower. - Share the code, or a link that has it: the game's address with
?join=and the code, likehttps://tide-engine.dev/towers/?join=K7QF2M. Whoever opens the link is in your room, with no lobby in the way. - Join. With only the code, a friend opens the game, presses Join a room, types or pastes the code (Ctrl+V, or Cmd+V) and presses Enter, or Join. A code nobody has, a room that's full and a match that ended each say so, on the page the code is typed on, to try again from.
In a game, Menu shows the room again: its code to copy, who's in it, and Leave the room, which takes you back to the lobby and a game of your own. A game you started alone has Invite friends there instead, which opens a room around the game as it is. While your menu is up, your tower builds by itself.
From the repo, with tide installed, the same game runs in a window, and joins the same rooms:
cd examples/towers
tide run # The lobby
tide run --host # Straight into a room of your own: its code shows over the Menu button
tide run --join K7QF2M # Straight into a friend's- A player takes the first plinth nobody has, with the tower that built itself there, and its wins. A fifth is turned away, and told the room is full.
- Joining in the middle of a round works. The match's world is sent as it is, with Box2D's whole state in it: every brick where it is and as fast, the ones in the air too. A plinth that's well under the other players' towers rises until it's level with the lowest of them, so whoever joins late is in the race.
- When the host leaves, the room goes on. Another player's machine takes the match over, with the towers as they were (the game's
hostMigrationsetting). The tower the host had builds by itself from then. - A match that ends puts you back on the lobby, with a line that says what happened, and four towers building behind it.
Only the players' inputs go over the network: where each wants their brick, how turned, and whether faster. Every machine runs the same Box2D from them, and gets the same towers, bit for bit (see Determinism). Your own brick answers at once however far away the host is, because your machine predicts the ticks the host hasn't confirmed yet, physics and all, and puts right what it guessed wrong about the others.
The code
Towers has five files of Tide, and a tide.packages that says box2d and rooms.
game.tideis the match, which every machine runs the same way. It's on this page, whole.bots.tideis how a tower nobody steers gets built: a plan for each brick, from how high the tower stands in each column.play.tideis what one machine shows of it: the hand on the brick, the camera and the pieces.effects.tidehas the dust, the pieces and the words that float up, andhud.tidethe race, the next brick and the words over the world.- The lobby and the menu are the
roomspackage's, the menu that comes with tide: which match this machine is in is its to say.
The physics
Everything that's solid is a body of Box2D's, made, moved and read through components, as the physics page says. A brick is one entity and one body from the moment it's dealt to the end of the round. What changes is what kind of body it is, and which shapes it has.
A falling brick is a kinematic body with no shapes. It goes where its velocity takes it, which is down, and where its position and angle are written, which is how a player steers it: half a cell across, a quarter turn. With no shape it touches nothing, and nothing touches it. What's in its way, it asks:
float Free(Physics physics, Kind kind, float2 from, int quarter, float2 way, float2 slack)
{
mut var free = 1.0;
var angle = Angle(quarter);
if (physics.Cast(Slim(First(kind), quarter, slack), from, angle, from + way, out var hit)) free = hit.fraction;
var second = Second(kind);
if (second.size.x > 0 && physics.Cast(Slim(second, quarter, slack), from, angle, from + way, out var other))
{
free = Math.Min(free, other.fraction);
}
return free;
}Cast moves a box along a way through the world, and says how much of the way it went before it touched something. The seven bricks are four cells each, which is one box for the bar and the square and two for the rest, so each box is moved, and the brick is free to go as far as the first of them. Slim makes a box a little smaller than it is: a shape that's cast counts as touching what it's flush with, and a brick has to slide down beside a wall it only just clears.
The system that steers it asks three things each tick. Is there room for the brick turned the way its player wants, which is the package's Overlap? Is it free to step half a cell across? And is it free to fall as far as this tick takes it?
if (falling.quarter != falling.wantQuarter && Room(physics, piece.kind, body.position, falling.wantQuarter))
{
falling.quarter = falling.wantQuarter;
body.angle = Angle(falling.quarter);
}
if (falling.column != falling.wantColumn && falling.age % 2 == 0)
{
var step = falling.wantColumn > falling.column ? 1 : -1;
if (Free(physics, piece.kind, body.position, falling.quarter, float2(step * 0.5, 0), float2(SLACK)) >= 1)
{
falling.column += step;
body.position.x = falling.home + falling.column * 0.5;
}
}
var speed = fast ? FAST : FALL;
var fall = speed * time.dt;
var free = Free(physics, piece.kind, body.position, falling.quarter, float2(0, -fall), float2(SLACK, 0));
if (free >= 1)
{
if (body.velocity.y != -speed) body.velocity = float2(0, -speed);
return;
}A turn or a step there's no room for doesn't happen, so a brick is never inside a wall and nothing is undone. The system is Before the package's stage: the world it asks is the world as the last tick's step left it, which is where body.position says the brick is, and the step that moves the brick is this tick's. controls is the input of the player who owns the brick.
Where it isn't free to fall, it lands: the same body is a dynamic one, with its shapes on it.
body.position.y -= fall * free;
body.velocity = float2(0, 0);
body.type = BodyType.Dynamic;
var first = First(piece.kind);
var second = Second(piece.kind);
this.Add(Box { size = first.size, offset = first.offset, friction = GRIP }, Brick { seat = falling.seat, born = time.tick });
if (second.size.x > 0) Spawn(Box { size = second.size, offset = second.offset, friction = GRIP }, Part { of = this });
this.Remove(Falling, Owner);It's put where it touches, at rest, however fast it fell: it was never in the tower, so nothing pushes it out, and a landing doesn't hit what's under it. Writing type is all it takes to change what a body is. Its first box is a Box on its own entity. An entity has one Box, so a brick of two gets the second as a Part: an entity of its own that names the brick, whose box is on the brick's body. From the next step the brick is Box2D's: it has weight, it rests on what's under it, and it goes over if that's not enough.
Where it will land is the same question, asked by a view. The outline under your brick is where Free says it stops, looking 60 meters down. A view asks the world like the match does, of the world it draws:
var fall = SIGHT * Free(physics, piece.kind, from, falling.quarter, float2(0, -SIGHT), float2(SLACK, 0));The plinths are kinematic bodies, which go where their velocity takes them, whatever stands on them. A quake is a plinth going there and back four times. A plinth rises under a player who joined late, and when a round is over the plinths sink, the winner's last, and everything on them comes down.
A tower is loose bricks all the way down. With the world as Box2D comes, forty meters of them is a spring: a contact gives a little, and every landing ran down the whole tower and back. So the game tunes the world, which is three fields of Physics:
physics.substeps = SUBSTEPS; // 8, where it comes with 4
physics.contactStiffness = STIFFNESS; // 60, where it comes with 30
physics.contactDamping = DAMPING; // 40, where it comes with 10Contacts are twice as stiff, which takes twice the substeps to count, and come back out four times as gently. A tower of loose bricks then stands at forty meters, and what's wrong with it low down still shows at the top.
Cement is a body's type again. Every brick at rest in the tower that uses it becomes a kinematic body, and goes where its plinth does from then on: the system that looks at every brick before the step gives it the plinth's velocity, so a quake carries it with what's built on it. When the round is over each is a dynamic body again, from the bottom of its tower up, and all of it comes down.
mut var solid = body;
solid.type = BodyType.Kinematic;
world.Set<Body>(this, solid);
brick.set = true;A gust is an impulse. That same system gives the loose bricks near the top of the tower a gust is against an impulse each tick it blows, more the nearer the top. It looks at every brick and writes a few, so it takes Body to read and puts the ones it changed back through the World:
if (Of(towers.gusts, lot) <= 0 || Math.Abs(body.position.x - Home(lot)) > PLINTH) return;
var depth = Of(towers.tops, lot) - Peak(body.position, body.angle, piece.kind);
if (depth > WIND_DEPTH) return;
mut var pushed = body;
pushed.impulse.x += Of(towers.winds, lot) * WIND * (1 - Math.Max(depth, 0) / WIND_DEPTH);
world.Set<Body>(this, pushed);How hard things meet is the package's to say. Hit is sent to both bodies that meet faster than the world's hitSpeed, which the game puts at 2.5 meters a second, with where and how fast. Nothing in the match handles it. Each machine makes pieces fly of it, more the harder it was: a brick that goes over the side and meets the ground, a tower coming down.
local event(Hit hit) Crash(Brick brick, mut Fx fx)
{
var hard = Math.Saturate((hit.speed - CRASH) / 12);
Chunks(fx, hit.point, 1 + int(hard * 3), 1.5 + hard * 4, SeatColor(brick.seat));
if (brick.seat == fx.seat && !brick.down) Shake(fx, 0.03 + hard * 0.1);
}How high a tower stands is read from its bricks: a system that runs for each plinth goes through every brick with a query, and takes the highest corner of the ones at rest over it. A brick that's under the plinths' tops, or well to one side of every plinth, is off its tower: it's marked, whoever put it there waits longer, and a few seconds later its entity is destroyed.
The input
A player's input isn't which keys they hold. It's where they want their brick, how turned, and whether faster:
input Controls
{
[Absolute] bool hands; // No lobby or menu is up on this machine
[Absolute] int serial;
[Absolute, Clamp(-REACH, REACH)] int column;
[Absolute, Clamp(0, 3)] int quarter;
bool drop;
...
}A view works those out a frame at a time, from the keys, the mouse and fingers, into a local singleton, and the input's Sample reads it. Saying where, not what, has three things going for it:
- A tap is never lost. A turn is "a quarter further than it was", which the next tick's input still says, where a button that was down for less than a tick might not.
- A guess stays put. A machine that runs ahead of the others guesses their inputs, and after 0.15 seconds takes them to have let go. These fields are marked
[Absolute], so the guess is that the other players' bricks stay on their way to where they were going. - One brick's steering isn't the next one's.
serialis the falling brick's number. The match only steers a brick by an input that names it, so the brick that's dealt as the last one lands doesn't start off to where the last one was sent.
The lobby
The lobby isn't the game's: it's the rooms package, the menu that comes with tide, which the game lists in its tide.packages. Main is the match's scene, so a match of this machine's own starts with the program, closed, and the menu is drawn over it: Play closes it, Host a room opens that match to others, and Join a room leaves it for someone else's. A machine that was told which match to be in, with --host or --join on its command line or ?host or ?join= in its page's address, has no menu in its way.
What the game adds is in hud.tide. While a page is up, the player's hands are off their brick: the input reads the menu's Lobby.playing and says so (hands), and the match builds their tower for them, as it does a tower nobody has. And a view tells the room's page about the match, who has each seat and whether bots play the rest, and sends the game's commands when the page's New game and Bots buttons say so:
view Roster(Towers towers, Session session, mut Lobby lobby)
{
lobby.seats = SEATS;
lobby.manned = 0;
lobby.mine = -1;
foreach (var entry in towers.seats)
{
lobby.manned |= 1 << entry.value;
if (entry.key == session.player) lobby.mine = entry.value;
}
lobby.bots = towers.bots;
lobby.offersNewGame = true;
lobby.offersBots = true;
if (lobby.newGame) Send(Rebuild { });
if (lobby.toggleBots) Send(Bots { });
}What a machine shows
- Whose tower is whose is the built-in
Ownercomponent, on a plinth and on the bricks it's dealt: the system that steers a falling brick takes the game's input, and gets the input of the player who owns the brick it runs for. A view picks this machine's player's withwith ownership. - Using what a tower earned, a new game and the bots' switch are commands, sent by buttons and handled in the match.
- The row of heights is read
verified: what the server has confirmed. They're on the plinths, so the view goes through them with a query. - The dust, the pieces and the camera's shake come from events. The match says what happens in it: a brick landed, one fell off, a tower earned something or used it, a round ended. The package says when two bodies meet hard. Nothing in the match handles those, so they cost it nothing, and each machine's local handlers make an effect of them, once, however often it runs the tick again. A round's winner waits for the server, with a
verifiedhandler. - A falling brick slides across and swings round, where the match steps it half a cell and turns it a quarter at once: where it's drawn is two local fields of the match's own component, which its view eases. A brick that just landed is lit the same way. Its outline where it will land is asked of the physics world, each frame.
- The camera follows your tower's top, and shows more of the tower the taller it stands. While a round is over it shows the whole race, to watch everything come down.
- Views see every body between its last two ticks, and gliding through a correction, because the package tags a
Body'spositionandanglefor it (see correction smoothing).
What it costs
Late in a round there are about 150 bodies: 120 bricks on the towers, loose but for the towers that used their Cement, and the rest falling or on the ground. On a desktop a tick takes around 0.07 ms while the towers build, with 0.03 more to keep and hash its world, 0.01 ms with everything at rest, and 0.1 ms while every tower comes down at once. In a browser those are 0.11, 0.04, 0.01 and 0.14. Box2D solves each tick in eight smaller steps here, twice what it comes with, which is what loose towers this tall take.
A machine that predicts runs ticks again when it guessed another player wrong, as many as it's ahead of the host by: ten of them, with Box2D's world made again from the match first and each tick's world kept and hashed, take 0.9 ms on a desktop and 1.2 ms in a browser, and 1.2 and 1.6 while everything comes down. That's a tenth of a frame at the worst, so there's room for a good deal more physics than the game asks for: what sets its size is how many bricks a player can place in a round, and how much of four towers fits in a small window.
Box2D's whole state is in the match, about 250 KB here, so a rollback, a player who joins and a new host all go on from it (see Multiplayer). A player who joins 75 seconds into a round is sent about 150 KB.
The match, whole
// Towers: four plinths side by side, and a race to build the tallest tower.
// Bricks come down one at a time. A player steers and turns the one that's
// falling, and where it touches anything it's a body like any other: it
// leans, slides and falls with whatever it's on. The first tower that stands
// over the finish line for three seconds takes the round. This file is the
// match, which every machine runs the same way. What a machine shows of it is
// in play.tide, effects.tide and hud.tide, and which match it's in is the
// rooms package's menu's to say. bots.tide is how a tower nobody steers gets
// built.
//
// Everything that's solid is Box2D's (the box2d package): the game spawns
// bodies, pushes them, says what kind each is, asks the world what's in a
// falling brick's way, and reads where they are.
using Box2D;
settings { title = "Towers"; appId = "dev.tide.towers"; maxPlayers = 4; hostMigration = true; stats = true; }
// The world, in meters. A brick's cell is one meter, the plinths' tops start
// at y = 0, and the world's middle is x = 0.
const int SEATS = 4; // Plinths, and the most players
const float PITCH = 11; // From one plinth's middle to the next
const float PLINTH = 6; // A plinth's width: six cells
const float DEPTH = 80; // How far down a plinth goes, so that it can rise and sink
const float GROUND = -7; // The ground between the plinths
const float FALLEN = -1.5; // A brick under this is off its tower...
const float SPILL = 1.5; // ...and so is one further than this past its plinth's side
const float FINISH = 40; // The line to build over
const int HOLD = 180; // ...and to stay over: three seconds
const int LIMIT = 5400; // After a minute and a half of a round, the line comes down...
const float LOWER = 0.004; // ...this far a tick, a meter every four seconds...
const float LOWEST = 12; // ...and no lower than this
const float DROP = 9; // How far over its tower a brick appears
const float FALL = 3.2; // A falling brick's speed, in meters a second
const float FAST = 18; // ...and while its player asks for more
const int REACH = 8; // How far from its plinth's middle a brick is steered, in half cells
const float SLACK = 0.1; // How much smaller a falling brick is taken to be than it is, where its way is asked for: it slides down beside a wall it only just clears
const float GRIP = 1; // A brick's friction
const float STEADY = 2; // ...and how fast its spin dies by itself
const int SUBSTEPS = 8; // The smaller steps Box2D solves a tick in: twice what it comes with...
const float STIFFNESS = 60; // ...which lets contacts be twice as stiff...
const float DAMPING = 40; // ...and they spring back four times as gently: a tower of loose bricks holds, forty meters up
const float CRASH = 2.5; // Bodies that meet faster than this say so, for the dust
const int PAUSE = 14; // Ticks from a brick landing to the next
const int STUN = 40; // ...and more for each brick that fell off the tower
const int STUN_MOST = 180;
const int RUBBLE = 600; // A fallen brick lies where it fell for ten seconds
const int GUST = 8; // Ticks a gust blows
const float WIND = 1.3; // What it gives a brick at the top of its tower each tick: an impulse
const float WIND_DEPTH = 3; // How far down the tower it's felt
const int QUAKE = 80; // Ticks a plinth shakes: four times there and back
const float SHAKE = 0.9; // ...and how fast at the most, in meters a second
const float RISE = 2.5; // How fast a plinth rises under a player who joined late
const float SINK = 14; // ...and sinks once the round is over
const int OVER = 420; // Ticks from a round's end to the next
const int STANDS = 170; // The winner's tower stands for these, then goes too
const float CRUMBLE = 0.8; // How fast what was set solid is let go, from the bottom up: meters a tick
enum Phase { Play, Over, Clear }
// The seven bricks. Each is four cells: one box, or two side by side.
enum Kind { I, O, T, L, J, S, Z }
// What a tower earns on its way up: a wind against the tallest of the others,
// all of its own bricks set solid, and every other plinth shaken.
enum Power { None, Gust, Cement, Quake }
// A box of a brick, in cells, around its body's place.
struct Block
{
float2 size;
float2 offset;
}
Block First(Kind kind)
{
switch (kind)
{
case Kind.I: return Block { size = float2(4, 1) };
case Kind.O: return Block { size = float2(2, 2) };
case Kind.S: return Block { size = float2(2, 1), offset = float2(-0.5, 0) };
case Kind.Z: return Block { size = float2(2, 1), offset = float2(0.5, 0) };
}
return Block { size = float2(3, 1) }; // T, L and J: a bar of three
}
// The second box, of no size for a brick that's one.
Block Second(Kind kind)
{
switch (kind)
{
case Kind.T: return Block { size = float2(1, 1), offset = float2(0, 1) };
case Kind.L: return Block { size = float2(1, 1), offset = float2(1, 1) };
case Kind.J: return Block { size = float2(1, 1), offset = float2(-1, 1) };
case Kind.S: return Block { size = float2(2, 1), offset = float2(0.5, 1) };
case Kind.Z: return Block { size = float2(2, 1), offset = float2(-0.5, 1) };
}
return Block { };
}
// The middle of a brick's cell, by its number from 0 to 3, around its body's place.
float2 Cell(Kind kind, int index)
{
var first = First(kind);
var count = int(first.size.x * first.size.y);
var part = index < count ? first : Second(kind);
var i = index < count ? index : index - count;
var wide = int(part.size.x);
return part.offset + float2(i % wide, i / wide) - (part.size - float2(1)) * 0.5;
}
// A falling brick turns by quarters, clockwise: where that leaves a cell.
float2 Turned(float2 cell, int quarter)
{
return quarter == 0 ? cell : quarter == 1 ? float2(cell.y, -cell.x) : quarter == 2 ? float2(-cell.x, -cell.y) : float2(-cell.y, cell.x);
}
float Angle(int quarter)
{
return quarter == 0 ? 0.0 : quarter == 1 ? -Math.PI / 2 : quarter == 2 ? Math.PI : Math.PI / 2;
}
// Where a brick's highest corner is.
float Peak(float2 position, float angle, Kind kind)
{
var s = Math.Sin(angle);
var c = Math.Cos(angle);
var half = 0.5 * (Math.Abs(s) + Math.Abs(c));
mut var peak = -1000.0;
for (var i = 0; i < 4; i++)
{
var cell = Cell(kind, i);
peak = Math.Max(peak, position.y + cell.x * s + cell.y * c + half);
}
return peak;
}
// The column a brick starts in, in half cells from its plinth's middle: a
// brick whose body is the middle of a cell is half a cell off the plinth's
// middle, so that its cells are over the plinth's.
int Start(Kind kind)
{
return kind == Kind.I || kind == Kind.O ? 0 : -1;
}
// Every tower is dealt the same bricks in the same order: each seven are the
// seven kinds, shuffled by the match's seed and the round.
Kind Dealt(int seed, int round, int index)
{
mut List<int> bag = [0, 1, 2, 3, 4, 5, 6];
for (var i = 6; i > 0; i--)
{
var other = Math.Hash(int4(seed, round, index / 7, i)) % (i + 1);
var held = bag[i];
bag[i] = bag[other];
bag[other] = held;
}
return Kind(bag[index % 7]);
}
float Home(int seat)
{
return (seat - (SEATS - 1) * 0.5) * PITCH;
}
// Whose plinth a place is over.
int Lot(float x)
{
return Math.Clamp(int(Math.Floor(x / PITCH + SEATS * 0.5)), 0, SEATS - 1);
}
int Of(int4 values, int seat)
{
return seat == 0 ? values.x : seat == 1 ? values.y : seat == 2 ? values.z : values.w;
}
float Of(float4 values, int seat)
{
return seat == 0 ? values.x : seat == 1 ? values.y : seat == 2 ? values.z : values.w;
}
int4 Put(int4 values, int seat, int value)
{
return int4(seat == 0 ? value : values.x, seat == 1 ? value : values.y, seat == 2 ? value : values.z, seat == 3 ? value : values.w);
}
float4 Put(float4 values, int seat, float value)
{
return float4(seat == 0 ? value : values.x, seat == 1 ? value : values.y, seat == 2 ? value : values.z, seat == 3 ? value : values.w);
}
// A plinth, and what's the seat's on it: a player who takes the seat goes on
// from its tower and its wins.
component Seat
{
int index;
float home; // Its middle, across
float base; // Its top, which rises under a player who joined late
float top; // Its tower's: the highest brick at rest on it
int above; // Ticks its tower has stood over the finish line
int wins;
int built = -1; // The round it was last made ready for
int dealt; // Bricks it was dealt this round
int serials; // ...and ever: each falling brick has a number of its own
Kind next; // The brick that comes after the one that's falling
bool busy; // A brick of its own is falling
int wait; // Ticks until its next one
int lost; // Bricks that fell off this round
int marks; // How many quarters of the way up it has been
Power power; // What it has earned, and not used
bool arrived; // A player just took it, in the middle of a round
bool lifted; // It rose for one already, this round
float lift; // Meters it still has to rise
// While nobody steers its bricks, it builds by itself (bots.tide). The
// engine seeds `dice` from the match's seed.
Random dice;
int think; // Ticks until it uses what it earned
}
// The brick that's falling, which its player steers. It's a kinematic body
// with no shapes yet: it goes where it's put and touches nothing, and what's
// in its way is asked of the world, each tick.
component Falling
{
int seat;
int serial; // Its number, which its player's input names
float home; // Its plinth's middle
int column; // Where it is, in half cells from there
int quarter; // How it's turned, in quarter turns clockwise
int wantColumn; // Where it's going, and how it's to be turned
int wantQuarter;
bool bot; // Nobody steers it: it follows what bots.tide planned
int hurry; // ...and falls fast from this tick of its fall
int age;
// This machine's own, for the view: where it's drawn across and how
// turned, on its way to where it is. Nothing of them is in the match.
local bool drawn;
local float across;
local float turned;
}
// What a body is the shape of: a falling brick's, and a landed one's.
component Piece { Kind kind; }
// A brick that landed: the same entity and the same body as when it fell, a
// dynamic one now, with its boxes on it, until it's set solid.
component Brick
{
int seat; // Whose it is: who put it there
int born; // The tick it landed in
bool down; // It fell off its tower
int rubble; // Ticks until it's cleared from the ground
bool set; // It's set solid: its body is a kinematic one, which goes where its plinth does
// This machine's own, for the view: how lit it is, just landed
local float lit;
}
component Ground { }
singleton Towers
{
Phase phase = Phase.Clear; // The first tick starts the first round
int round;
int built; // Rounds started ever, which a new game doesn't count again from 1
int timer;
int clock; // Ticks this round has been played
float line = FINISH; // Where the line is: it comes down on a round that goes on long
int winner = -1; // The seat whose tower took the round, while it's over
bool bots = true; // Towers nobody steers are built by themselves
float4 tops; // Each tower's top, by seat, as the last tick left it
float4 sways; // How fast each plinth moves across this tick, and up
float4 rises;
int4 gusts; // Ticks left of the wind against each
int4 winds; // ...and which way it blows, -1 or 1
int4 quakes; // Ticks left of each plinth's shaking
int4 sets; // The towers whose bricks all set solid this tick
List<Entity> plinths; // By seat
Map<PlayerID, int> seats; // Each player's
}
scene Main { }
// What a player does is where they want their brick and how turned, not which
// keys they hold: another machine's guess of it stays where it was last, and a
// press that's lost on the way changes nothing. `serial` says which brick
// that's about, so that the next one doesn't start where the last was sent.
input Controls
{
[Absolute] bool hands; // No lobby or menu is up on this machine
[Absolute] int serial;
[Absolute, Clamp(-REACH, REACH)] int column;
[Absolute, Clamp(0, 3)] int quarter;
bool drop;
Sample(Devices devices, Hand hand)
{
hands = hand.playing;
serial = hand.serial;
column = hand.column;
quarter = hand.quarter;
drop = hand.playing && hand.drop;
}
}
// Sent to a plinth: a player took its seat, its falling brick landed, a brick
// of its tower fell off, it used what it earned, its tower took the round.
event Seated { }
event Placed { }
event Dropped { }
event Spent { }
event Won { }
// What happens in the match, said for whoever shows it: nothing in the match
// handles these, so they cost it nothing. Each machine's handlers of them are
// in effects.tide. Landed is sent to the brick that landed.
event Landed { float2 at; int seat; float speed; }
event Lost { float2 at; int seat; }
event Earned { int seat; Power power; }
event Struck { int seat; Power power; int target; }
event RoundOver { int round; int winner; }
event RoundBuilt { int round; }
// What the buttons ask of the match.
command Use { }
command Rebuild { }
command Bots { }
// The player who has a seat, or no player.
PlayerID Sitter(Towers towers, int seat)
{
foreach (var entry in towers.seats)
{
if (entry.value == seat) return entry.key;
}
return default;
}
// What stays from round to round: the ground, and the plinths. A plinth is a
// kinematic body: it goes where its velocity takes it, whatever stands on it.
// And how the world is tuned, which is the package's Physics to say: stiffer
// contacts than it comes with, that spring back less, for towers of loose
// bricks forty meters high.
event(Spawned) Build(mut Towers towers, mut Physics physics, with Main)
{
physics.substeps = SUBSTEPS;
physics.contactStiffness = STIFFNESS;
physics.contactDamping = DAMPING;
physics.hitSpeed = CRASH;
Spawn(Body { type = BodyType.Static, position = float2(0, GROUND - 0.5) }, Box { size = float2(400, 1), friction = GRIP }, Ground);
for (var seat = 0; seat < SEATS; seat++)
{
var plinth = Spawn(Body { type = BodyType.Kinematic, position = float2(Home(seat), -DEPTH / 2) },
Box { size = float2(PLINTH, DEPTH), friction = GRIP }, Seat { index = seat, home = Home(seat) });
towers.plinths.Add(plinth);
}
}
// A player takes the first seat nobody has. Owner ties its plinth to them, and
// the bricks it's dealt from then on.
event(PlayerJoined joined) Sit(mut Towers towers)
{
if (towers.seats.Has(joined.player)) return;
for (var seat = 0; seat < SEATS; seat++)
{
if (Sitter(towers, seat) != default) continue;
towers.seats[joined.player] = seat;
towers.plinths[seat].Add(Owner { player = joined.player });
towers.plinths[seat].Send(Seated { });
return;
}
}
event(PlayerLeft left) Stand(mut Towers towers)
{
if (!towers.seats.TryGet(left.player, out var seat)) return;
towers.plinths[seat].Remove(Owner);
towers.seats.Remove(left.player);
}
// A falling brick's box as the world is asked about it: `slack` smaller, across
// and up as the world has them, whichever way the brick is turned. A shape
// that's cast touches what it's flush with, so the box is a little narrower
// than the brick for its way down, to slide beside a wall the brick only just
// clears, and a little smaller all round to be moved across or turned.
Box Slim(Block block, int quarter, float2 slack)
{
var less = quarter % 2 == 0 ? slack : float2(slack.y, slack.x);
return Box { size = block.size - less, offset = block.offset };
}
// How much of a way a falling brick is free to go, from 0 to 1. Each of its
// boxes is moved along the way (the package's Cast, which changes nothing),
// and where the first of them touches something is as far as the brick goes.
// It has no shapes of its own in the world yet, so nothing it meets is itself.
float Free(Physics physics, Kind kind, float2 from, int quarter, float2 way, float2 slack)
{
mut var free = 1.0;
var angle = Angle(quarter);
if (physics.Cast(Slim(First(kind), quarter, slack), from, angle, from + way, out var hit)) free = hit.fraction;
var second = Second(kind);
if (second.size.x > 0 && physics.Cast(Slim(second, quarter, slack), from, angle, from + way, out var other))
{
free = Math.Min(free, other.fraction);
}
return free;
}
// Whether there's room for a falling brick somewhere, turned some way: nothing
// is in its boxes there (the package's Overlap).
bool Room(Physics physics, Kind kind, float2 at, int quarter)
{
var angle = Angle(quarter);
if (physics.Overlap(Slim(First(kind), quarter, float2(SLACK)), at, angle, out _)) return false;
var second = Second(kind);
return second.size.x <= 0 || !physics.Overlap(Slim(second, quarter, float2(SLACK)), at, angle, out _);
}
// A falling brick goes where its player wants it, half a cell at a time, and
// turns by quarters, wherever it has room to. `controls` is the input of the
// player who owns it. This runs before the package's systems, so the step that
// moves it is this tick's, and what it asks of the world is the world as the
// last tick's step left it.
//
// It falls by its body's velocity, and is put across and turned by having its
// body written. Each tick it asks how far down it's free to go. All the way,
// and it goes on falling. Less, and it has landed: it's put where it touches,
// at rest, its body is made a dynamic one, and its boxes are put on it, the
// first as its own Box and the second, for the bricks that have one, as a Part
// of it. It's the same entity and the same body from the moment it's dealt
// to the end of the round.
[Before(PhysicsStages.Step)]
system Steer(Controls controls, mut Falling falling, mut Body body, Piece piece, Towers towers, Physics physics, Time time)
{
if (towers.phase != Phase.Play || body.position.y < GROUND - 6)
{
if (towers.phase == Phase.Play) towers.plinths[falling.seat].Send(Placed { });
this.Destroy();
return;
}
falling.age += 1;
mut var fast = falling.bot && falling.age > falling.hurry && falling.column == falling.wantColumn;
if (!falling.bot && controls.serial == falling.serial)
{
falling.wantColumn = controls.column;
falling.wantQuarter = controls.quarter;
fast = controls.drop;
}
if (falling.quarter != falling.wantQuarter && Room(physics, piece.kind, body.position, falling.wantQuarter))
{
falling.quarter = falling.wantQuarter;
body.angle = Angle(falling.quarter);
}
if (falling.column != falling.wantColumn && falling.age % 2 == 0)
{
var step = falling.wantColumn > falling.column ? 1 : -1;
if (Free(physics, piece.kind, body.position, falling.quarter, float2(step * 0.5, 0), float2(SLACK)) >= 1)
{
falling.column += step;
body.position.x = falling.home + falling.column * 0.5;
}
}
var speed = fast ? FAST : FALL;
var fall = speed * time.dt;
var free = Free(physics, piece.kind, body.position, falling.quarter, float2(0, -fall), float2(SLACK, 0));
if (free >= 1)
{
if (body.velocity.y != -speed) body.velocity = float2(0, -speed);
return;
}
body.position.y -= fall * free;
body.velocity = float2(0, 0);
body.type = BodyType.Dynamic;
var first = First(piece.kind);
var second = Second(piece.kind);
this.Add(Box { size = first.size, offset = first.offset, friction = GRIP }, Brick { seat = falling.seat, born = time.tick });
if (second.size.x > 0) Spawn(Box { size = second.size, offset = second.offset, friction = GRIP }, Part { of = this });
this.Remove(Falling, Owner);
towers.plinths[falling.seat].Send(Placed { });
this.Send(Landed { at = body.position, seat = falling.seat, speed = speed });
}
// A plinth moves by its velocity: it shakes in a quake, rises under a player
// who joined late, and sinks once the round is over, the winner's last. And a
// new round makes its seat ready. How fast it moves is left in Towers, for
// the bricks that are set solid on it.
[Before(PhysicsStages.Step)]
system Move(mut Seat seat, mut Body body, mut Towers towers, Time time)
{
mut var velocity = float2(0, 0);
if (towers.phase == Phase.Play && seat.built != towers.built)
{
seat.built = towers.built;
seat.above = 0;
seat.dealt = 0;
seat.next = Dealt(Random.seed, towers.round, 0);
seat.busy = false;
seat.wait = 30 + seat.index * 6;
seat.lost = 0;
seat.marks = 0;
seat.power = Power.None;
seat.arrived = false;
seat.lifted = false;
seat.lift = 0;
if (towers.round == 1) seat.wins = 0;
body.position = float2(seat.home, -DEPTH / 2);
this.Snap();
}
else if (towers.phase == Phase.Over)
{
var stands = seat.index == towers.winner && towers.timer > OVER - STANDS;
if (!stands && body.position.y > -DEPTH / 2 + GROUND - 3) velocity.y = -SINK;
}
else if (towers.phase == Phase.Play)
{
if (seat.arrived)
{
// Up to the lowest of the other players' towers, once a round
seat.arrived = false;
mut var lowest = 1000.0;
foreach (var entry in towers.seats)
{
if (entry.value != seat.index) lowest = Math.Min(lowest, Of(towers.tops, entry.value));
}
var rise = Math.Floor(Math.Min(lowest, FINISH * 0.6) - seat.top);
if (lowest < 1000 && rise >= 1 && !seat.lifted)
{
seat.lift = rise;
seat.lifted = true;
seat.marks = Math.Max(seat.marks, int((seat.top + rise) * 4 / FINISH));
}
}
var quake = Of(towers.quakes, seat.index);
if (quake > 0) velocity.x = SHAKE * Math.Sin((QUAKE - quake) * Math.TAU / 20);
else if (body.position.x != seat.home) body.position.x = seat.home;
if (seat.lift > 0)
{
velocity.y = RISE;
seat.lift -= RISE * time.dt;
}
}
if (body.velocity.x != velocity.x || body.velocity.y != velocity.y) body.velocity = velocity;
var carries = towers.phase == Phase.Play;
towers.sways = Put(towers.sways, seat.index, carries ? velocity.x : 0.0);
towers.rises = Put(towers.rises, seat.index, carries ? velocity.y : 0.0);
}
// What's done to bricks before the step. One that's set solid goes where its
// plinth does: its body is a kinematic one, so it's given the plinth's velocity. A
// gust pushes the loose ones near the top of the tower it's against, with an
// impulse each tick it blows: more, the nearer the top. This looks at every
// brick and writes a few, so it takes Body to read and puts the ones it
// changed back through the World.
[Before(PhysicsStages.Step)]
system Push(Body body, Piece piece, Brick brick, Towers towers, mut World world)
{
if (brick.down) return;
var lot = Lot(body.position.x);
if (brick.set)
{
var velocity = float2(Of(towers.sways, lot), Of(towers.rises, lot));
if (body.velocity.x == velocity.x && body.velocity.y == velocity.y) return;
mut var carried = body;
carried.velocity = velocity;
world.Set<Body>(this, carried);
return;
}
if (Of(towers.gusts, lot) <= 0 || Math.Abs(body.position.x - Home(lot)) > PLINTH) return;
var depth = Of(towers.tops, lot) - Peak(body.position, body.angle, piece.kind);
if (depth > WIND_DEPTH) return;
mut var pushed = body;
pushed.impulse.x += Of(towers.winds, lot) * WIND * (1 - Math.Max(depth, 0) / WIND_DEPTH);
world.Set<Body>(this, pushed);
}
// How high each tower stands: its highest brick that's at rest over its
// plinth. The query goes through every brick, for each plinth. A tower that's
// a quarter of the way further up than it has been earns something.
[After(PhysicsStages.Step)]
system Measure(mut Seat seat, Body body, Towers towers, query (Body at, Piece piece, Brick brick) bricks)
{
seat.base = body.position.y + DEPTH / 2;
mut var top = seat.base;
foreach (var entry in bricks)
{
if (entry.brick.down || entry.at.position.y + 2.5 < top || Math.Abs(entry.at.position.x - seat.home) > PLINTH / 2 + 1) continue;
if (!entry.brick.set && Math.LengthSq(entry.at.velocity) > 0.16) continue;
top = Math.Max(top, Peak(entry.at.position, entry.at.angle, entry.piece.kind));
}
seat.top = top;
if (towers.phase != Phase.Play) return;
seat.above = top >= towers.line ? seat.above + 1 : 0;
if (seat.marks < 3 && top >= FINISH * (seat.marks + 1) / 4)
{
seat.marks += 1;
seat.power = Power(seat.marks);
seat.think = 50 + seat.dice.NextInt(140);
Send(Earned { seat = seat.index, power = seat.power });
}
}
// A brick that's under the plinths' tops, or well to the side of every
// plinth, is off its tower: whoever put it there waits longer for their next
// one, and it lies where it falls a while before it's cleared. An entity
// that's destroyed takes its body with it.
//
// Every brick of a tower is loose, from the plinth up, unless its tower's
// Cement set it solid: that sets every brick that's at rest in the tower, as
// if its mortar had. A brick that's set has its body made a kinematic one,
// and when the round is over a dynamic one again, from the bottom of its
// tower up: what comes down then is all of it. This looks at every brick and
// changes a few, so it takes Body to read and puts the ones it changed back
// through the World.
[After(PhysicsStages.Step)]
system Tend(Body body, mut Brick brick, Towers towers, mut World world)
{
if (towers.phase == Phase.Clear)
{
this.Destroy();
return;
}
if (brick.down)
{
brick.rubble -= 1;
if (brick.rubble <= 0 && towers.phase == Phase.Play) this.Destroy();
return;
}
var lot = Lot(body.position.x);
if (brick.set)
{
var since = OVER - towers.timer - (lot == towers.winner ? STANDS : 0);
if (towers.phase != Phase.Over || body.position.y > since * CRUMBLE) return;
mut var loose = body;
loose.type = BodyType.Dynamic;
world.Set<Body>(this, loose);
brick.set = false;
return;
}
var aside = Math.Abs(body.position.x - Home(lot)) - PLINTH / 2;
if (body.position.y < FALLEN || aside > SPILL)
{
brick.down = true;
brick.rubble = RUBBLE;
if (towers.phase != Phase.Play) return;
towers.plinths[brick.seat].Send(Dropped { });
Send(Lost { at = body.position, seat = brick.seat });
return;
}
if (towers.phase != Phase.Play || aside > 1 || Math.LengthSq(body.velocity) > 0.04 || Math.Abs(body.spin) > 0.2) return;
if (Of(towers.sets, lot) <= 0) return;
mut var solid = body;
solid.type = BodyType.Kinematic;
world.Set<Body>(this, solid);
brick.set = true;
}
// A seat is dealt its next brick once the last has landed and its wait is
// over. `controls` is the input of the player who has the seat: while their
// lobby or menu is up, or with nobody there, the tower builds by itself.
[After(PhysicsStages.Step)]
system Deal(Controls controls, mut Seat seat, Towers towers, query (Body at, Piece piece, Brick brick) bricks)
{
if (towers.phase != Phase.Play || seat.built != towers.built) return;
var player = Sitter(towers, seat.index);
var hands = player != default && controls.hands;
if (!hands && !towers.bots) return;
if (!hands && seat.power != Power.None)
{
seat.think -= 1;
if (seat.think <= 0) this.Send(Spent { });
}
if (seat.busy) return;
if (seat.wait > 0)
{
seat.wait -= 1;
return;
}
var kind = seat.next;
seat.dealt += 1;
seat.next = Dealt(Random.seed, towers.round, seat.dealt);
seat.serials += 1;
seat.busy = true;
var serial = seat.serials * SEATS + seat.index;
var at = float2(seat.home + Start(kind) * 0.5, Math.Ceil(seat.top) + DROP);
if (hands)
{
Spawn(Body { type = BodyType.Kinematic, position = at, velocity = float2(0, -FALL), angularDamping = STEADY }, Piece { kind = kind },
Falling { seat = seat.index, serial = serial, home = seat.home, column = Start(kind), wantColumn = Start(kind) },
Owner { player = player });
return;
}
// For the plan: how high the tower stands in each of the plinth's six
// columns, and whether the brick that's on top there lies askew
mut List<float> heights = [0, 0, 0, 0, 0, 0];
mut List<bool> askew = [false, false, false, false, false, false];
foreach (var entry in bricks)
{
if (entry.brick.down || Math.Abs(entry.at.position.x - seat.home) > PLINTH / 2 + 2) continue;
var s = Math.Sin(entry.at.angle);
var c = Math.Cos(entry.at.angle);
var half = 0.5 * (Math.Abs(s) + Math.Abs(c));
for (var i = 0; i < 4; i++)
{
var cell = Cell(entry.piece.kind, i);
var column = int(Math.Floor(entry.at.position.x + cell.x * c - cell.y * s - seat.home + PLINTH / 2));
var height = entry.at.position.y + cell.x * s + cell.y * c + half - seat.base;
if (column < 0 || column >= 6 || height <= heights[column]) continue;
heights[column] = height;
askew[column] = Math.Abs(s * c) > 0.1;
}
}
// No two towers are built alike: each seat is so careful, and so quick, for a round
var temper = Math.Hash(int3(Random.seed, towers.round, seat.index));
var plan = Think(kind, heights, askew, temper % 100 * 0.003, seat.dice);
Spawn(Body { type = BodyType.Kinematic, position = at, velocity = float2(0, -FALL), angularDamping = STEADY }, Piece { kind = kind },
Falling { seat = seat.index, serial = serial, home = seat.home, column = Start(kind), wantColumn = plan.column,
wantQuarter = plan.quarter, bot = true, hurry = 20 + temper / 100 % 30 + seat.dice.NextInt(30) });
}
event(Seated) Arrive(mut Seat seat, Towers towers)
{
seat.arrived = towers.phase == Phase.Play;
}
event(Placed) Next(mut Seat seat)
{
seat.busy = false;
seat.wait = Math.Max(seat.wait, PAUSE);
}
event(Dropped) Stall(mut Seat seat)
{
seat.lost += 1;
seat.wait = Math.Min(seat.wait + STUN, STUN_MOST);
}
event(Won) Win(mut Seat seat)
{
seat.wins += 1;
}
// A seat uses what it earned. The gust is against the tallest of the other
// towers, and blows away from this one.
event(Spent) Spend(mut Seat seat, mut Towers towers)
{
if (seat.power == Power.None || towers.phase != Phase.Play) return;
var power = seat.power;
seat.power = Power.None;
mut var target = seat.index;
if (power == Power.Gust)
{
mut var tallest = -1000.0;
for (var other = 0; other < SEATS; other++)
{
if (other == seat.index || Of(towers.tops, other) <= tallest) continue;
tallest = Of(towers.tops, other);
target = other;
}
towers.gusts = Put(towers.gusts, target, GUST);
towers.winds = Put(towers.winds, target, target > seat.index ? 1 : -1);
}
else if (power == Power.Quake)
{
for (var other = 0; other < SEATS; other++)
{
if (other != seat.index) towers.quakes = Put(towers.quakes, other, QUAKE);
}
}
else
{
towers.sets = Put(towers.sets, seat.index, 1);
}
Send(Struck { seat = seat.index, power = power, target = target });
}
// The rounds. A tower that has stood over the line long enough takes the
// round, the tallest if there are several; the plinths sink, and everything
// comes down; then all of it goes, and the next round starts. A round that
// goes on long ends all the same: the line comes down to the towers.
[After(PhysicsStages.Step)]
system Rounds(mut Towers towers, query (Seat seat) seats)
{
mut var winner = -1;
mut var tallest = 0.0;
foreach (var entry in seats)
{
towers.tops = Put(towers.tops, entry.seat.index, entry.seat.top);
if (entry.seat.above < HOLD || entry.seat.top <= tallest) continue;
tallest = entry.seat.top;
winner = entry.seat.index;
}
towers.gusts = Math.Max(towers.gusts - int4(1), int4(0));
towers.quakes = Math.Max(towers.quakes - int4(1), int4(0));
towers.sets = int4(0);
if (towers.phase == Phase.Play)
{
towers.clock += 1;
towers.line = Math.Max(FINISH - Math.Max(towers.clock - LIMIT, 0) * LOWER, LOWEST);
if (winner < 0) return;
towers.phase = Phase.Over;
towers.timer = OVER;
towers.winner = winner;
towers.plinths[winner].Send(Won { });
Send(RoundOver { round = towers.round, winner = winner });
return;
}
if (towers.phase == Phase.Over)
{
towers.timer -= 1;
if (towers.timer <= 0) towers.phase = Phase.Clear;
return;
}
towers.round += 1;
towers.built += 1;
towers.phase = Phase.Play;
towers.clock = 0;
towers.line = FINISH;
towers.winner = -1;
towers.gusts = int4(0);
towers.quakes = int4(0);
Send(RoundBuilt { round = towers.round });
}
// A player uses what their tower earned.
command(Use use, PlayerID sender) UsePower(Towers towers)
{
if (towers.seats.TryGet(sender, out var seat)) towers.plinths[seat].Send(Spent { });
}
// The New game button: a first round again, with nobody's wins.
command(Rebuild rebuild, PlayerID sender) Reset(mut Towers towers)
{
towers.phase = Phase.Clear;
towers.round = 0;
}
command(Bots bots, PlayerID sender) Idle(mut Towers towers)
{
towers.bots = !towers.bots;
}