Kiste
DE

Kistris

A complete Tetris — seven pieces with wall-kick, a ghost piece, level speed-up, sound, music and a high-score list with names. All in a single Kiste file, with no game engine.

Kistris in play

Build it yourself, for free

Kistris is open source. Download the source, open it in the Kiste IDE (or build it from the command line with kiste build kistris.ki) — it's the same code the finished game is made from. The five sounds and the title image are included.

Prefer to build it step by step and learn Kiste from the ground up? There's a whole book for that: Learn Kiste with Kistris.

How it's built

Kistris is pure Kiste. It uses only the built-in modules gui (windows, drawing, keys), klang, liste, zufall, wert, datei and text — no external game engine. Here are the most interesting parts.

1 · The playfield is a flat list

Instead of a two-dimensional grid, Kistris keeps the whole field as one long list of 200 cells. The cell at column x, row y lives at index y · BREITE + x. Two small functions hide that arithmetic — after that it feels like a grid.

// The playfield is ONE flat list, not a 2-D grid.
// Cell (x, y) lives at index y * BREITE + x.
funktion feld_hole(x, y) {
    gib wert.als_ganz(feld[y * BREITE + x])
}
funktion feld_setze(x, y, w) {
    feld[y * BREITE + x] = w
}

2 · Seven pieces from a table

Each of the seven pieces has four orientations. Rather than rotating them at runtime, every shape sits ready in a table — four rotations, four blocks each as (x, y) pairs relative to the pivot. form_wert is the lookup service.

// One flat list per piece type: 4 rotations x 4 blocks x (x, y).
// Clockwise rotation is already baked into the table.
funktion form_wert(t, r, k) {
    nimm idx = r * 8 + k
    wenn t == 1 { gib FORM_I[idx] }   // I piece
    wenn t == 2 { gib FORM_O[idx] }   // O piece
    wenn t == 3 { gib FORM_T[idx] }   // T piece
    // … 4=S · 5=Z · 6=J
    gib FORM_L[idx]                   // L piece
}

3 · One question decides everything: does it fit?

Moving, rotating, dropping, game over — it all hangs on a single check: does the piece in orientation r at (bx, by) collide with a wall, the floor or a settled block? Rotation with "wall-kick" simply asks the same question again a few cells over.

// A single question decides everything: does the piece fit here?
funktion kollidiert(t, r, bx, by) {
    für k = 0 bis 3 {
        nimm x = bx + form_wert(t, r, k * 2)
        nimm y = by + form_wert(t, r, k * 2 + 1)
        wenn x < 0 oder x >= BREITE { gib 1 }   // wall
        wenn y >= HÖHE { gib 1 }                // floor
        wenn y >= 0 {
            wenn feld_hole(x, y) != 0 { gib 1 } // another block
        }
    }
    gib 0
}

4 · Clearing rows without shifting

The nicest trick: a full row isn't laboriously "copied down". Because the field is a flat list, we cut out the row's ten cells and prepend ten zeros — and the whole stack above slides down one line at once.

// Clearing a full row: cut out the 10 cells and PREPEND 10 zeros —
// everything above slides down one line automatically.
für k = 1 bis BREITE { liste.entferne(feld, y * BREITE) }
für k = 1 bis BREITE { liste.stelle_voran(feld, 0) }

5 · Fair randomness: the 7-bag

Real Tetris doesn't roll pieces purely at random. A "bag" holds each of the seven types exactly once in random order; when it's empty, it refills. So you never get the same piece ten times, and never wait forever for the long bar.

// The 7-bag: a "bag" holding types 1-7 in random order.
// Fairer than pure random — never 10x the same piece, never an I-drought.
funktion fülle_beutel() {
    nimm kandidaten = [1, 2, 3, 4, 5, 6, 7]
    solange liste.länge(kandidaten) > 0 {
        nimm z = zufall.ganz(0, liste.länge(kandidaten) - 1)
        liste.hänge_an(beutel, wert.als_ganz(kandidaten[z]))
        liste.entferne(kandidaten, z)
    }
}

6 · High-score list — typed after the game

The best results land in a text file and survive a restart. Like the old arcade machines, the player types their name after the round, straight onto the screen — no text field that steals the keys mid-game. The key handler assembles the name letter by letter itself.

// The name is typed AFTER the game — no text field that would
// steal the keys during play. The handler reads the letters itself:
wenn taste == "eingabe" {
    speichern()                                   // Enter saves the score
} sonstwenn taste == "BackSpace" {
    eingabe_name = text.erste_n(eingabe_name, länge(eingabe_name) - 1)
} sonstwenn länge(taste) == 1 {
    wenn länge(eingabe_name) < 14 { eingabe_name = "{eingabe_name}{taste}" }
}

That's the whole recipe: a flat list, a couple of tables, one collision question and a timer. The full source is included in the download above — build it, change the colours, invent your own piece.