So what did not change?
Four screens, four ways of driving it, and one directory that none of them touched. This is that directory — thirteen files and a thousand lines, and it is worth reading, which is not a sentence anyone gets to write about most code.
Nothing below changed to put the game on a console, in a browser, in a window or on a phone.
What the domain looks like
01One question, asked of everything
A piece, the pile and the wall are the same kind of thing
This is the whole of collision detection.
private bool Collides(Piece candidate) =>
candidate.Intersects(Frame) || candidate.Intersects(Pile);domain/Well.cs
Not three special cases — one test, asked twice. The falling piece, the pile of landed blocks and the boundary of the well all answer the same question: do you occupy this cell? Everything else follows from refusing to treat them as different.
02The walls are not an if
The boundary is a figure too — it just happens to be infinite
Most Tetris code checks the walls with arithmetic written especially for walls. This one makes the boundary answer the same question the pieces do.
public override bool Occupies(Position position) =>
position.Column < 0
|| position.Column >= Width
|| position.Row >= Height;domain/Frame.cs
It cannot list its own cells — it is unbounded, and its `Cells` property throws to say so. So it answers as a rule instead of as a set, and collision never notices the difference: the four-cell piece is always the one that iterates, and the boundary is only ever asked.
03No coordinate arithmetic
A piece does not move by adding one to x
There are coordinates — it is a grid. What there is not, is arithmetic on them.
internal void MoveLeft() => Shift(Offset.Left);
internal void MoveRight() => Shift(Offset.Right);domain/Well.cs
It moves *left*, and left is a named thing — `Offset.Left`, declared once beside `Down` and `Right`. In the three hundred and forty-three lines of the well, exactly two read a row or a column number: one takes the anchor a piece spawns at, the other asserts an invariant. Neither of them moves anything.
04Shapes, not matrices
The T is four drawings, not a rotation matrix
Each of the seven tetrominoes states its own poses, in its own file, as the cells it occupies.
protected override Position[] LocalCells(int orientationIndex) => orientationIndex switch
{
0 => [new(0, 1), new(1, 0), new(1, 1), new(1, 2)],
1 => [new(0, 1), new(1, 1), new(1, 2), new(2, 1)],
2 => [new(1, 0), new(1, 1), new(1, 2), new(2, 1)],
_ => [new(0, 1), new(1, 0), new(1, 1), new(2, 1)],
};domain/Pieces.cs
Point up, right, down, left. The square declares one pose because rotating it changes nothing; the bar declares two. A subclass adds geometry and never mechanism — the rotating, the translating and the four-cell invariant live once, in the piece they all inherit from.
05What is not in here
No database, no console, no logging, no wiring
Searched across all thirteen files:
DbContext 0
System.Data 0
Console. 0
ILogger 0
IServiceCollection 0domain/ — all thirteen files
The domain does not know where it is running, what is watching it, or where anything is kept. That is not discipline applied on top; there is nowhere in here for any of it to go. The things it would have needed — somewhere to store the game, somewhere to send the board, something to pick the next piece — live in the host, and a host is the only thing that changes when the game moves to a new screen.
06The surface
One public type in the whole library, and it is empty
Everything that carries a rule is internal. This is all a caller can see:
public sealed class TetrisDomain
{
}domain/TetrisDomain.cs
It exists so a host can name the assembly in C# without hunting for a type. That is its entire job — and the file's own comment records the check: built with it deleted, the domain exposes no public type at all and still runs. From outside, there is no way to hand the well an illegal move, because there is no public `Position` to build one out of.
What the comparison build needed
Not a list of patterns we decided were unnecessary — a list of what the comparison had to add, counted in the laboratory that built it.
| What the orthodox build needed | Here |
|---|---|
| A port for where the game is kept between operationsIGameStatePort | none — the well is in memory and each verb is journaled |
| A port for where the board goesIBoardOutputPort, and a BoardView to carry it | none — the domain emits nothing; a host reads a snapshot |
| A port for which piece comes nextIPieceSelectionPort, and an adapter to satisfy it | none — the choice is a rule, and its randomness is recorded |
| An application service in front of the modelGameService | none — the verbs are on the thing itself |
| A factory and a restore constructor, to rebuild the game from parts56 lines added and 5 removed, inside the rule model | none — replay re-enters through the same verbs a player used |
| Stand-ins, so the rules can be tested at allthree of them — and without them 20 of the 64 tests do not run | none — there is nothing to stand in for |
The 56-line figure is the one that lab's README calls the most attackable in the paper, and it names the two files to read before running anything. Two of the four claims it was built to check came back refuted and one tied — it corrects the paper more than it confirms it.
Four of these did not disappear; they moved. A board still gets serialised and still gets written somewhere — in the host, beside the code that already cared about files and sockets. What changed is that none of it is in the rules any more.
Read the whole thing
Thirteen files, published with the paper under Apache 2.0. The nine laboratories that measure everything claimed about it are in the same repository.
13 files · 1,035 linesMuch of that is the comments explaining why each decision was made, which is the part worth reading twice.
The domain on GitHubAnd how do we know it did not change?
A diff over the domain directory, across fifteen commits, coming back empty — plus an orthodox ports-and-adapters build of the same game, made for the comparison, which refuted two of the four things it was built to confirm.
The evidenceOne domain. Many stages.
The console didn’t need its own Tetris.
Neither did the browser, the desktop or the phone.
They all drove the same domain.
The stage changed. The domain didn’t.