Games & Interactive Apps

Ready-to-run game starters. Each script is a playable game you can extend with new rooms, questions, or units without touching the core engine.

Back to Kookbook


Text Adventure

A three-room dungeon crawler with branching navigation, a locked door, and an inventory system. Demonstrates how a Class bundles player state, how Dictionary stores room data keyed by location string, and how List:Contains gates progress.

Modules: Konsol List Dictionary Class

Usage:

minks text_adventure.ks

Sample session:

DUNGEON ESCAPE
Find the gold coin hidden in the vault. Type 'help' for commands.

=== Cave Entrance ===
Cold air and damp stone walls surround you. A passage leads east.
On the floor: torch.

> take
You pick up the torch.
> go east

=== Winding Corridor ===
A narrow passage runs east and west. The eastern door is heavy iron.
On the floor: iron key.

> take
You pick up the iron key.
> go east
You use the iron key. The heavy door grinds open.

=== Ancient Vault ===
Dust-covered carvings line the walls. A stone altar stands in the centre.
On the floor: gold coin.

> take
You pick up the gold coin.

The gold catches the torchlight. You have won!

Script:

// text_adventure.ks - Branching text adventure with inventory and locked doors
// Usage: minks text_adventure.ks
//
// Three rooms laid out west → east:
//   Cave Entrance  ·  Winding Corridor  ·  Ancient Vault (locked until you find the key)
//
// Collect the iron key in the corridor to unlock the vault.
// Pick up the gold coin inside to win.
//
// Commands: look · go east · go west · take · inventory · quit
//
// Modules: Konsol, List, Dictionary, Class

// ── Player class ──────────────────────────────────────────────────────────────
// Bundling all mutable player state into a class makes it easy to extend later
// (e.g. add a "stamina" or "damage" field without touching game logic).

Class:Create(Player) {
    Var:String location;    // current room: "entrance", "corridor", or "vault"
    Var:Number hp;

    function isAlive() : Boolean {
        return hp > 0;
    }
}

// ── Room data stored in Dictionaries ──────────────────────────────────────────
// One Dictionary per property. The player's location string is the key for all three,
// so a single Dictionary:Get call retrieves the right data for wherever the player is.

Dictionary:New roomNames;
Dictionary:Set("entrance", "Cave Entrance",    roomNames);
Dictionary:Set("corridor", "Winding Corridor", roomNames);
Dictionary:Set("vault",    "Ancient Vault",    roomNames);

Dictionary:New roomDescs;
Dictionary:Set("entrance", "Cold air and damp stone walls surround you. A passage leads east.", roomDescs);
Dictionary:Set("corridor", "A narrow passage runs east and west. The eastern door is heavy iron.", roomDescs);
Dictionary:Set("vault",    "Dust-covered carvings line the walls. A stone altar stands in the centre.", roomDescs);

// Floor items are removed from this Dictionary once the player picks them up.
// An empty string means the floor is clear.
Dictionary:New floorItems;
Dictionary:Set("entrance", "torch",     floorItems);
Dictionary:Set("corridor", "iron key",  floorItems);
Dictionary:Set("vault",    "gold coin", floorItems);

// ── Player and inventory ───────────────────────────────────────────────────────

Class:Player player;
player.location = "entrance";
player.hp = 100;

List:New inventory:String;

// ── Room display helper ────────────────────────────────────────────────────────
// Receives all data as parameters so it does not need to touch global Dictionaries.

function showRoom(String rName, String rDesc, String rItem) {
    Konsol:Print("");
    Konsol:Print("=== " + rName + " ===");
    Konsol:Print(rDesc);
    if (rItem != "") {
        Konsol:Print("On the floor: " + rItem + ".");
    }
}

// ── Pre-declared working variables ────────────────────────────────────────────

Var:String input;
Var:String cmd;
Var:String rName;
Var:String rDesc;
Var:String rItem;
Var:String loc;
Var:String invItem;
Var:Boolean running = true;
Var:Boolean hasKey;
Var:Number invSize;

// ── Opening screen ────────────────────────────────────────────────────────────

Konsol:Print("DUNGEON ESCAPE");
Konsol:Print("Find the gold coin hidden in the vault. Type 'help' for commands.");

Dictionary:Get(player.location, roomNames, rName);
Dictionary:Get(player.location, roomDescs, rDesc);
Dictionary:Get(player.location, floorItems, rItem);
showRoom(rName, rDesc, rItem);

// ── Main game loop ────────────────────────────────────────────────────────────

while (running) {
    Konsol:Input("> ", input);
    String:Trim(input, cmd);
    String:Lower(cmd, cmd);

    if (cmd == "quit" || cmd == "q") {
        Konsol:Print("Farewell, adventurer.");
        running = false;

    } else if (cmd == "help") {
        Konsol:Print("Commands: look · go east · go west · take · inventory · quit");

    } else if (cmd == "look" || cmd == "l") {
        Dictionary:Get(player.location, roomNames, rName);
        Dictionary:Get(player.location, roomDescs, rDesc);
        Dictionary:Get(player.location, floorItems, rItem);
        showRoom(rName, rDesc, rItem);

    } else if (cmd == "inventory" || cmd == "inv" || cmd == "i") {
        List:Size(inventory, invSize);
        if (invSize == 0) {
            Konsol:Print("You are carrying nothing.");
        } else {
            Konsol:Print("Carrying:");
            for (Number k = 0; k < invSize; k++) {
                List:Get(k, inventory, invItem);
                Konsol:Print("  - " + invItem);
            }
        }

    } else if (cmd == "take" || cmd == "get" || cmd == "pick up") {
        // Read what is on the floor, then clear it from the Dictionary.
        Dictionary:Get(player.location, floorItems, rItem);
        if (rItem == "") {
            Konsol:Print("There is nothing here to take.");
        } else {
            List:Push(rItem, inventory);
            Dictionary:Set(player.location, "", floorItems);
            Konsol:Print("You pick up the " + rItem + ".");
            if (rItem == "gold coin") {
                Konsol:Print("");
                Konsol:Print("The gold catches the torchlight. You have won!");
                running = false;
            }
        }

    } else if (cmd == "go east" || cmd == "east" || cmd == "e") {
        loc = player.location;
        if (loc == "entrance") {
            player.location = "corridor";
            Dictionary:Get(player.location, roomNames, rName);
            Dictionary:Get(player.location, roomDescs, rDesc);
            Dictionary:Get(player.location, floorItems, rItem);
            showRoom(rName, rDesc, rItem);
        } else if (loc == "corridor") {
            // The vault door is locked - the iron key is required.
            List:Contains("iron key", inventory, hasKey);
            if (hasKey) {
                player.location = "vault";
                Konsol:Print("You use the iron key. The heavy door grinds open.");
                Dictionary:Get(player.location, roomNames, rName);
                Dictionary:Get(player.location, roomDescs, rDesc);
                Dictionary:Get(player.location, floorItems, rItem);
                showRoom(rName, rDesc, rItem);
            } else {
                Konsol:Print("The iron door is locked. You need a key.");
            }
        } else {
            Konsol:Print("There is no passage to the east.");
        }

    } else if (cmd == "go west" || cmd == "west" || cmd == "w") {
        loc = player.location;
        if (loc == "corridor") {
            player.location = "entrance";
            Dictionary:Get(player.location, roomNames, rName);
            Dictionary:Get(player.location, roomDescs, rDesc);
            Dictionary:Get(player.location, floorItems, rItem);
            showRoom(rName, rDesc, rItem);
        } else if (loc == "vault") {
            player.location = "corridor";
            Dictionary:Get(player.location, roomNames, rName);
            Dictionary:Get(player.location, roomDescs, rDesc);
            Dictionary:Get(player.location, floorItems, rItem);
            showRoom(rName, rDesc, rItem);
        } else {
            Konsol:Print("There is no passage to the west.");
        }

    } else {
        Konsol:Print("Unknown command. Type 'help' for a list.");
    }
}


Trivia Quiz

Five general-knowledge questions presented in a random order every run. Demonstrates parallel List collections, a Fisher-Yates shuffle over an Array index, and Math:Random + Math:Floor for integer random numbers.

Modules: Math Konsol List Array

Usage:

minks trivia_quiz.ks

Sample session:

TRIVIA QUIZ
Five questions. Enter 1, 2, or 3 for your answer.

Question 1 of 5: Which planet is closest to the Sun?
  1) Earth
  2) Venus
  3) Mercury
Answer: 3
Correct!

Question 2 of 5: How many sides does a hexagon have?
  1) 4
  2) 6
  3) 8
Answer: 1
Wrong! The answer was: 6

...

You scored 4 out of 5.
Good effort!

Script:

// trivia_quiz.ks - Interactive trivia quiz with shuffled questions and score tracking
// Usage: minks trivia_quiz.ks
//
// Five questions, each with three choices. Enter 1, 2, or 3 to answer.
// Questions are presented in a random order each run via a Fisher-Yates shuffle.
//
// Modules: Math, Konsol, List, Array

// ── Questions and answers ─────────────────────────────────────────────────────
// Five parallel Lists: one for question text, one per answer option, one for the
// correct choice (1 = A, 2 = B, 3 = C). Index i across all lists = question i.

List:New questions:String;
List:Push("What is the capital of France?",                   questions);
List:Push("How many sides does a hexagon have?",              questions);
List:Push("Which planet is closest to the Sun?",              questions);
List:Push("What gas do plants absorb during photosynthesis?", questions);
List:Push("Who wrote Romeo and Juliet?",                      questions);

List:New optA:String;
List:Push("London",  optA);
List:Push("4",       optA);
List:Push("Earth",   optA);
List:Push("Oxygen",  optA);
List:Push("Dickens", optA);

List:New optB:String;
List:Push("Berlin",          optB);
List:Push("6",               optB);
List:Push("Venus",           optB);
List:Push("Carbon dioxide",  optB);
List:Push("Shakespeare",     optB);

List:New optC:String;
List:Push("Paris",    optC);
List:Push("8",        optC);
List:Push("Mercury",  optC);
List:Push("Nitrogen", optC);
List:Push("Chaucer",  optC);

// Correct answer: 1 = A, 2 = B, 3 = C
List:New answers:Number;
List:Push(3, answers);   // Paris      = C
List:Push(2, answers);   // 6          = B
List:Push(3, answers);   // Mercury    = C
List:Push(2, answers);   // CO₂        = B
List:Push(2, answers);   // Shakespeare = B

// ── Shuffle the question order (Fisher-Yates) ─────────────────────────────────
// Build an index array [0, 1, 2, 3, 4] then shuffle it in-place.
// Using a separate index array means the five parallel lists stay untouched;
// we just look up order[q] at quiz time instead of q directly.

Array:New order[5]:Number;
for (Number k = 0; k < 5; k++) {
    order[k] = k;
}

Var:Number swapI = 4;
Var:Number swapJ;
Var:Number swapTemp;
Var:Number rnd;

while (swapI > 0) {
    // Pick a random index in [0, swapI] by taking a float in [0, swapI+1) and flooring it.
    Math:Random(swapI + 1, rnd);
    Math:Floor(rnd, swapJ);
    // Swap order[swapI] ↔ order[swapJ]
    swapTemp = order[swapI];
    order[swapI] = order[swapJ];
    order[swapJ] = swapTemp;
    swapI = swapI - 1;
}

// ── Pre-declared working variables ────────────────────────────────────────────

Var:Number score = 0;
Var:Number qIdx;
Var:String qText;
Var:String a;
Var:String b;
Var:String c;
Var:Number correct;
Var:Number answer;
Var:Number qNum;

// ── Quiz loop ─────────────────────────────────────────────────────────────────

Konsol:Print("TRIVIA QUIZ");
Konsol:Print("Five questions. Enter 1, 2, or 3 for your answer.");
Konsol:Print("");

for (Number q = 0; q < 5; q++) {
    // Use the shuffled index to look up question data.
    qIdx = order[q];

    List:Get(qIdx, questions, qText);
    List:Get(qIdx, optA, a);
    List:Get(qIdx, optB, b);
    List:Get(qIdx, optC, c);
    List:Get(qIdx, answers, correct);

    qNum = q + 1;
    Konsol:Print("Question ${qNum} of 5: ${qText}");
    Konsol:Print("  1) ${a}");
    Konsol:Print("  2) ${b}");
    Konsol:Print("  3) ${c}");

    // Konsol:Input stores the value as a Number when the input looks like one.
    Konsol:Input("Answer: ", answer);

    if (answer == correct) {
        Konsol:Print("Correct!");
        score = score + 1;
    } else {
        // Tell the player what the right answer was.
        if (correct == 1) { Konsol:Print("Wrong! The answer was: " + a); }
        if (correct == 2) { Konsol:Print("Wrong! The answer was: " + b); }
        if (correct == 3) { Konsol:Print("Wrong! The answer was: " + c); }
    }
    Konsol:Print("");
}

// ── Final score ───────────────────────────────────────────────────────────────

Konsol:Print("You scored ${score} out of 5.");

if (score == 5) {
    Konsol:Print("Perfect score! Outstanding!");
} else if (score >= 3) {
    Konsol:Print("Good effort!");
} else {
    Konsol:Print("Better luck next time.");
}


Turn-Based Strategy

A two-unit skirmish on a 5×5 tile grid. You command the Knight [K]; the Guard [G] uses a simple one-step AI that moves toward you and strikes automatically when adjacent. Demonstrates Class for units, a flat Array for the map grid, and Math:Random for combat variation.

Modules: Array List Dictionary Math Class

Usage:

minks strategy_game.ks

Sample session:

BATTLE ON THE GRID
[K] = you (Knight)   [G] = enemy (Guard)   [#] = wall
Move: n s e w    Attack: a

Turn 1  |  Knight HP: 30/30   Guard HP: 25/25
+---------------+
|[K] .  . [#] . |
| .  .  . [#] . |
| .  .  .  .  . |
| .  .  . [#] . |
| .  .  .  . [G]|
+---------------+
> e
> e
...
> a
You strike the Guard for 11 damage! (Guard HP: 14)
The Guard strikes you for 7 damage! (Your HP: 23)

Script:

// strategy_game.ks - Turn-based strategy on a 5×5 grid
// Usage: minks strategy_game.ks
//
// You are the Knight [K]. Defeat the Guard [G] before it defeats you.
//
// Each turn:
//   n / s / e / w  - move one tile
//   a              - attack (only works when adjacent to the Guard)
//
// The Guard moves one step toward you every turn and strikes automatically
// when adjacent. Combat deals base damage plus a small random bonus.
//
// Modules: Array, List, Map, Math, Class

// ── Unit class ────────────────────────────────────────────────────────────────
// All unit state lives here. Methods keep HP clamped at zero.

Class:Create(Unit) {
    Var:String name;
    Var:Number hp;
    Var:Number maxHp;
    Var:Number attack;
    Var:Number x;
    Var:Number y;

    function isAlive() : Boolean {
        return hp > 0;
    }

    function takeDamage(Number dmg) {
        hp = hp - dmg;
        if (hp < 0) { hp = 0; }
    }
}

// ── Grid ──────────────────────────────────────────────────────────────────────
// 5×5 open ground. Cell (col, row) maps to flat index: row * 5 + col.
// Value 0 = open, 1 = wall. Extend this to add terrain features.

Array:New grid[25]:Number;
// Add a short wall in the middle column to force flanking manoeuvres.
grid[7]  = 1;   // (col 2, row 1)
grid[12] = 1;   // (col 2, row 2)
grid[17] = 1;   // (col 2, row 3)

// ── Units ─────────────────────────────────────────────────────────────────────

Class:Unit knight;
knight.name   = "Knight";
knight.hp     = 30;
knight.maxHp  = 30;
knight.attack = 8;
knight.x      = 0;
knight.y      = 0;

Class:Unit guard;
guard.name   = "Guard";
guard.hp     = 25;
guard.maxHp  = 25;
guard.attack = 6;
guard.x      = 4;
guard.y      = 4;

// ── Pre-declared working variables ────────────────────────────────────────────

Var:Boolean running = true;
Var:String  input;
Var:String  cmd;
Var:Number  newX;
Var:Number  newY;
Var:Number  ex;
Var:Number  ey;
Var:Number  dx;
Var:Number  dy;
Var:Number  absDx;
Var:Number  absDy;
Var:Number  dist;
Var:Number  dmg;
Var:Number  rndBonus;
Var:Number  tileIdx;
Var:String  line;
Var:Number  turn = 1;
Var:Number  khp;
Var:Number  ghp;

// ── Game loop ─────────────────────────────────────────────────────────────────

Konsol:Print("BATTLE ON THE GRID");
Konsol:Print("[K] = you (Knight)   [G] = enemy (Guard)   [#] = wall");
Konsol:Print("Move: n s e w    Attack: a");
Konsol:Print("");

while (running) {

    // ── Draw the 5×5 grid ─────────────────────────────────────────────────────
    // Read HP into plain vars so they can be used inside ${}.
    khp = knight.hp;
    ghp = guard.hp;
    Konsol:Print("Turn ${turn}  |  Knight HP: ${khp}/${knight.maxHp}   Guard HP: ${ghp}/${guard.maxHp}");
    Konsol:Print("+---------------+");
    for (Number row = 0; row < 5; row++) {
        line = "|";
        for (Number col = 0; col < 5; col++) {
            if (knight.x == col && knight.y == row) {
                line = line + "[K]";
            } else if (guard.x == col && guard.y == row) {
                line = line + "[G]";
            } else {
                tileIdx = row * 5 + col;
                if (grid[tileIdx] == 1) {
                    line = line + "[#]";
                } else {
                    line = line + " . ";
                }
            }
        }
        line = line + "|";
        Konsol:Print(line);
    }
    Konsol:Print("+---------------+");

    // ── Player input ──────────────────────────────────────────────────────────

    Konsol:Input("> ", input);
    String:Trim(input, cmd);
    String:Lower(cmd, cmd);

    // ── Move ──────────────────────────────────────────────────────────────────

    newX = knight.x;
    newY = knight.y;

    if      (cmd == "n") { newY = newY - 1; }
    else if (cmd == "s") { newY = newY + 1; }
    else if (cmd == "e") { newX = newX + 1; }
    else if (cmd == "w") { newX = newX - 1; }

    if (cmd == "n" || cmd == "s" || cmd == "e" || cmd == "w") {
        if (newX < 0 || newX > 4 || newY < 0 || newY > 4) {
            Konsol:Print("You cannot move that way - edge of the grid.");
        } else if (newX == guard.x && newY == guard.y) {
            Konsol:Print("The Guard blocks that tile. Use 'a' to attack.");
        } else {
            tileIdx = newY * 5 + newX;
            if (grid[tileIdx] == 1) {
                Konsol:Print("A wall blocks the way.");
            } else {
                knight.x = newX;
                knight.y = newY;
            }
        }
    }

    // ── Attack ────────────────────────────────────────────────────────────────

    if (cmd == "a" || cmd == "attack") {
        dx = knight.x - guard.x;
        dy = knight.y - guard.y;
        Math:Abs(dx, absDx);
        Math:Abs(dy, absDy);
        dist = absDx + absDy;

        if (dist > 1) {
            Konsol:Print("The Guard is too far away. Move adjacent first.");
        } else {
            // Add a random bonus (0–3) to keep each fight unpredictable.
            Math:Random(4, rndBonus);
            Math:Floor(rndBonus, rndBonus);
            dmg = knight.attack + rndBonus;
            guard.takeDamage(dmg);
            ghp = guard.hp;
            Konsol:Print("You strike the Guard for ${dmg} damage! (Guard HP: ${ghp})");
        }
    }

    // ── Check win ─────────────────────────────────────────────────────────────

    if (guard.isAlive() == false) {
        Konsol:Print("");
        Konsol:Print("The Guard collapses. Victory!");
        running = false;
    }

    // ── Enemy turn ────────────────────────────────────────────────────────────

    if (running) {
        // Move one step toward the Knight (horizontal first, then vertical).
        dx = knight.x - guard.x;
        dy = knight.y - guard.y;

        ex = guard.x;
        ey = guard.y;

        if      (dx > 0) { ex = ex + 1; }
        else if (dx < 0) { ex = ex - 1; }
        else if (dy > 0) { ey = ey + 1; }
        else if (dy < 0) { ey = ey - 1; }

        // Validate: in bounds, not a wall, not the Knight's tile.
        if (ex >= 0 && ex <= 4 && ey >= 0 && ey <= 4) {
            tileIdx = ey * 5 + ex;
            if (grid[tileIdx] != 1) {
                if (ex != knight.x || ey != knight.y) {
                    guard.x = ex;
                    guard.y = ey;
                }
            }
        }

        // Attack if adjacent after moving.
        dx = knight.x - guard.x;
        dy = knight.y - guard.y;
        Math:Abs(dx, absDx);
        Math:Abs(dy, absDy);
        dist = absDx + absDy;

        if (dist <= 1) {
            Math:Random(3, rndBonus);
            Math:Floor(rndBonus, rndBonus);
            dmg = guard.attack + rndBonus;
            knight.takeDamage(dmg);
            khp = knight.hp;
            Konsol:Print("The Guard strikes you for ${dmg} damage! (Your HP: ${khp})");
        }

        // Check lose.
        if (knight.isAlive() == false) {
            Konsol:Print("");
            Konsol:Print("You have fallen in battle. Game over.");
            running = false;
        }

        turn = turn + 1;
    }
}


Back to Kookbook