A guide to the Tenet language
This guide is going to walk you through the Tenet language by working on a small project; a dungeon crawler game.
Installing
You don’t need to. This guide has examples you can run directly, or copy over to the editor tab. The browser-based interpreter has identical functionality to the Java interpreter.
If you’d like to install the Java version of Tenet, go to
the releases page where we link
directly to the Maven package. It’s easiest to download the tenet-x.x.x-all.jar and then
run it directly with a recent JRE.
Caveat: the --enable-preview option is required to run Tenet.
$ java --enable-preview -jar tenet-0.6.4-all.jar --help
Tenet interpreter; (C) 2026 Ben Samuel
Usage: tenet -c source
Evaluates source passed in as a command line option.
Usage: tenet <file>
Evaluate source read from file. If file is the single
dash, reads from stdin.
Usage: tenet --show-invoke
Shows the java command needed to run 'tenet'.
Usage: tenet -h
Shows this help information.
$ java --enable-preview -jar tenet-0.6.4-all.jar -c 'do { print-line("Hello world."); }'
Hello world.
Writing the Gruesome Caverns
Our game will be named the Gruesome Caverns, a classic dungeon crawler. We’ll touch on some basic ideas in Tenet to give you a sense of how coding in Tenet feels. This guide isn’t aimed at an absolute beginner, but if you feel comfortable writing some Python or Javascript, it should make sense.
Set up the fundamentals
As with most games, we’ll need to manage some state. We do that in Tenet
with a record, and we’ll call ours the Game type.
// Our game has a fairly minimal amount of data.
// Quite a bit of other information can be determined from this.
type Game = (
hp: Int,
xp: Int,
enemy: Int
);
fun new-game() => Game {
// Set up the game object with default values.
let mut game: Game = (hp: 0, xp: 0, enemy: 0);
// Then set our character's HP to the max.
game.hp = stat-max-hp(game);
return game;
}
// For now, we'll just make this 10.
fun stat-max-hp(game: Game) => Int {
return 10;
}
do {
let game = new-game();
show("A new game: ", game);
}
If you’re not familiar with role-playing games,
hpmeans “hitpoints” andxpmeans “experience points.” These numbers track our character’s health and advancement in skill, respectively.
Also, the enemy field tracks the toughness of enemies, and works
similarly to xp, so as our character gets tougher, so do his foes.
Let’s walk through what this code does. The type Game statement declares a new type.
Tenet is structurally typed so it’s not strictly necessary to declare types. In this
project, we won’t use a lot of fancy types. Types are homoiconic to values, meaning
they look similar, and you can see in the new-game function that the literal value
form of the Game type looks similar.
An important detail the new-game function takes care of is that the hero’s hitpoints
need to be greater than 0. We could just set them to one, but we’ll be nice and
start our hero off fully healed. To do this, we construct a zeroed out Game value,
but make it “mutable” with let mut. Then we can update it by assigning the
correct value of hp.
A little math can make our life simpler
Steady progression makes games fun, and a common way to do this is through
experience points and levelling. We’ll handle this by defining a relation
between experience and levels by a factor. So the xp is
while level
is .
These are straightforward enough to implement. At present, Tenet only supports
integer arithmetic. The builtin sqrt-int panics if a negative value is passed in
as there’s no integer solution.
// Hero's XP/level scaling factor
let hero-factor = 5;
// Slimes get XP as you go deeper into the dungeon
let enemy-factor = 3;
// Given an amount of xp, determine the level of a thing.
fun xp-to-level(xp: Int, factor: Int) => Int {
if xp lt 0 { return 0; }
return sqrt-int(xp / factor);
}
// Given a level, determine how much XP is necessary to attain it.
fun level-to-xp(level: Int, factor: Int) => Int {
return level ** 2 * factor;
}
do {
show("At 1000 xp, our level is ", xp-to-level(1000, hero-factor));
show("At level 20, our xp is ", level-to-xp(20, hero-factor));
}
Meet our hero, Biff the … Tiny?
Okay, let’s combine this to see all of our hero’s stats.
// Our game state
type Game = (hp: Int, xp: Int, enemy: Int);
fun new-game() => Game {
let mut game: Game = (hp: 0, xp: 0, enemy: 0);
game.hp = stat-max-hp(game);
return game;
}
// Hero's XP/level scaling factor
let hero-factor = 5;
// Slimes get XP as you go deeper into the dungeon
let enemy-factor = 3;
// Given an amount of xp, determine the level of a thing.
fun xp-to-level(xp: Int, factor: Int) => Int {
if xp lt 0 { return 0; }
return sqrt-int(xp / factor);
}
// Given a level, determine how much XP is necessary to attain it.
fun level-to-xp(level: Int, factor: Int) => Int {
return level ** 2 * factor;
}
// Get the current HP of our hero.
fun stat-hp(game: Game) => Int {
return game.hp;
}
// Get the current XP of our hero.
fun stat-xp(game: Game) => Int {
return game.xp;
}
// Get the level of our hero.
fun stat-level(game: Game) => Int {
return xp-to-level(game.xp, hero-factor);
}
// Determine the attack strength of our hero.
fun stat-bonk(game: Game) => Int {
return 3 * (stat-level(game) + 1);
}
// Determine the max HP of our hero.
fun stat-max-hp(game: Game) => Int {
return 5 * (stat-level(game) + 1);
}
fun show-stats(game: Game) {
let hp = int-to-str(stat-hp(game));
let max-hp = int-to-str(stat-max-hp(game));
let xp = int-to-str(stat-xp(game));
let bonk = int-to-str(stat-bonk(game));
let level' = stat-level(game);
let level = int-to-str(level');
let xp-advance = int-to-str(level-to-xp(level' + 1, hero-factor));
let title = level' ? {
0 -> "Tiny";
1 -> "Scrawny";
2 -> "Okay";
_ -> "Mighty";
};
print-line("Name: Biff the " ++ title);
print-line("======================");
print-line("HP: " ++ hp ++ "/" ++ max-hp ++ " Level: " ++ level);
print-line("XP: " ++ xp ++ "/" ++ xp-advance);
print-line("");
print-line("Skills");
print-line("------");
print-line("Bonk: " ++ bonk ++ " Stratigraphy: 13");
}
do {
show-stats(new-game());
print-line("");
show-stats((hp: 20, xp: 1000, enemy: 800));
}
Let’s take note of a few items:
The stat- functions. Some stats like hp and xp are directly recorded,
so the stat-hp() function can just read game.hp.
Contrast that with our stat-max-hp and stat-bonk functions, which calculate the hero’s level
and then adjust that to compute the value of the actual stat.
The show-stats function. The int-to-str converts an integer to a string; there’s
a related str-to-int function as well.
We’re also using the ? operator to decide what title Biff has. This is called the “when”
operator, so named because it works almost identically to the when statement.
They are very similar, but they solve different problems. The
whenstatement decides what the program should do, while the?operator is more about what value should be produced.
The game loop
As with most programs doing user-interfaces, games are generally a loop responding to user feedback.
We’re going to have two loops, we’ll do the outer exploring loop first. One feature we’d like is for the user to be able to quit the game at any point, and for this we’ll use an idea in Tenet called escaping.
// Our game state
type Game = (hp: Int, xp: Int, enemy: Int);
fun new-game() => Game {
let mut game: Game = (hp: 0, xp: 0, enemy: 0);
game.hp = 10;
return game;
}
fun explore(game: Game) => normal ~ Game | #quit {
print-line("We haven't implemented explore, so Biff can't do that.");
return normal ~ game;
}
fun rest(game: Game) => normal ~ Game | #quit {
print-line("ZZZZzzzzz... haven't implemented resting yet.");
return normal ~ game;
}
fun show-stats(game: Game) {
print-line("Biff the Mighty");
print-line("===============");
print-line("((We already did the character sheet, and it's quite long.))");
}
fun show-help() {
print-line("Welcome to the Gruesome Caverns.");
print-line("");
print-line("While exploring, type 'help' for this help,");
print-line(" 'explore' to explore deeper,");
print-line(" 'rest' to risk resting,");
print-line(" 'stats' to see your stats,");
print-line(" 'quit' to quit,");
print-line("or just the first letter.");
print-line("");
print-line("In a fight, type 'attack' or 'flee'.");
}
fun read-command(prompt: Str) => Str {
// The read-line builtin displays a prompt and
// reads a line of text the user enters.
return read-line(prompt) ? {
#eof -> "q";
line ~ "" -> "";
// As mentioned above, we only care about
// a single letter.
line ~ let cmd -> lowercase(cmd[0]);
};
}
// Main loop controlling the game.
fun game-loop() => #quit {
let mut game = new-game();
// Show help initially to orient the user.
show-help();
// We'll loop until our hero dies, or the user quits.
while true {
// We simplify longer commands to the first letter.
when read-command(prompt: "e/r/s/h/q?") {
"e" -> game = explore(game) ! normal;
"r" -> game = rest(game) ! normal;
"s" -> show-stats(game);
"h" -> show-help();
"q" -> esc #quit;
_ -> print-line("Invalid command");
}
// If the player's health drops to zero,
// the game is over.
if game.hp le 0 {
print-line("Alas, you have fallen.");
esc #quit;
}
}
}
do {
game-loop();
print-line("Thanks for playing!");
}
State pattern and Escaping
Let’s draw attention to these lines:
when read-command(prompt: "e/r/s/h/q?") {
"e" -> game = explore(game) ! normal;
"r" -> game = rest(game) ! normal;
...
"q" -> esc #quit;
}
We’ll use the state pattern of game = modify(game) a bit more: pass the whole game
state to a function, which in turns modifies it and return the updated state.
Here, though, we wanted the user to be able to quit the game from within an encounter. So both
explore and rest return a union type normal ~ Game | #quit.
Reading a union
Unions in Tenet are always tagged . The two tags here are normal, indicating the game
should continue normally, and quit indicating the user wants to quit.
Much as game.hp gets the hitpoints field of the game record, explore(game) ! normal asserts that the union
value has the normal tag and gets its Game variant. If it doesn’t have that tag, it escapes regular control flow. You can also see that if we match
the "q" pattern, we call esc #q.
The zen of unions in Tenet
A substantial aspect of programming is about different people agreeing on a contract, and a major difficulty is that the contract is often developed over time on live systems.
Tenet argues that a good strategy is to:
- plainly state what you mean
- don’t overstate or add unnecessary details
In Haskell, the Either monad is quite elegant:
data Either a b = Left a | Right b
But Left and Right don’t plainly state what you mean and the conventions around Either that
Left is an error while Right is a normal value add unnecessary details.
In Tenet, #quit simply means “the user wants to quit.” Then the author of the main loop can
decide what to do with it.
This isn’t the whole story, of course, but we hope it’s a solid foundation.
Implementing explore and rest
In the code above, we started our game loop, and our character can choose between two actions, explore and rest . The idea is that Biff is either exploring deeper into the cavern, or he’s resting and regaining his strength. Exploring should make the enemies tougher, and since he’s moving around, he’s got a higher chance of running into a slime. Resting should restore some health, and since he’s staying put, there’s a lesser chance a slime will find him.
But, either way, if Biff does encounter a slime, we invoke a common encounter function.
type Game = (hp: Int, xp: Int, enemy: Int);
fun encounter(game: Game) => normal ~ Game | #quit {
print-line("Encounter stubbed out.");
return normal ~ game;
}
fun heal-hp(game: Game, max-heal: Int) => Game {
// Stub this by simply using max-heal.
let mut game' = game;
game'.hp += max-heal;
return game';
}
// Have our character venture deeper and fight slimes.
fun explore(game: Game) => normal ~ Game | #quit {
print-line("You venture deeper into the dark depths.");
let mut game' = game;
// Going deeper increases the enemy strength
game'.enemy += 1;
if random(bound: 100) lt 75 {
print-line("You find a slime!");
return encounter(game');
} else {
print-line("The winding dungeon tunnels are eerily empty.");
return normal ~ game';
}
}
// Have our character pause to recover health, but possibly be found by a slime.
fun rest(game: Game) => normal ~ Game | #quit {
print-line("You rest.");
let mut game' = game;
if random(bound: 100) lt 25 {
game' = heal-hp(game', max-heal:5);
print-line("Your rest is interrupted by a squishing sound!");
return encounter(game');
} else {
game' = heal-hp(game', max-heal:15);
print-line("Thankfully, no slime finds you.");
return normal ~ game';
}
}
do {
let game = (hp: 10, xp: 100, enemy: 20);
when explore(game) {
normal ~ let game' -> show("After exploring: ", game');
#quit -> print-line("User quit");
}
when rest(game) : normal ~ let game' -> show("After resting: ", game');
}
Mutability in Tenet
We’ve used the let mut game' = game; ... return game idiom again here. Parameters are all
passed as immutable copies of what the caller passed in. The function can then work with
a mutable copy, and return an immutable copy.
We bring this up because there’s a benefit for you as a progammer: you get the predictability and safety of an immutable language, while “game.hp += 5” just works.
Handling unions with when
Our do block at the bottom demonstrates using when to match any potential value and act on it.
Healing
Before we get to the encounter, we’ll implement healing. The core of it is we need to increment
the .hp field, so most of it is figuring out how much, don’t heal past max HP, provide
feedback to the player, and so on.
// Hiding some functions we've already seen.
type Game = (hp: Int, xp: Int, enemy: Int);
fun stat-hp(game: Game) => Int {
return game.hp;
}
fun stat-max-hp(game: Game) => Int {
return 5 * (stat-level(game) + 1);
}
let hero-factor = 5;
fun stat-level(game: Game) => Int {
return xp-to-level(game.xp, hero-factor);
}
fun xp-to-level(xp: Int, factor: Int) => Int {
if xp lt 0 { return 0; }
return sqrt-int(xp / factor);
}
// Restore some HP.
fun heal-hp(game: Game, max-heal: Int) => Game {
let hp = stat-hp(game);
let max-hp = stat-max-hp(game);
let mut new-hp = hp + random(bound: max-heal);
if new-hp gt max-hp {
new-hp = max-hp;
}
if new-hp gt hp {
print-line("You heal for " ++ int-to-str(new-hp - hp) ++ " HP");
}
let mut game' = game;
game'.hp = new-hp;
return game';
}
do {
let before = (hp: 5, xp: 300, enemy: 400);
show("before: ", before);
let after = heal-hp(before, 15);
show("after: ", after);
}
Loops in loops: the encounter
Now we’re getting into the guts of the game, the encounter. We first set up some stats, including the slime’s hp, and a mutable copy of the game state.
As with the main game loop, the encounter loop prompts the user for commands and then processes them.
In each round of fighting, Biff has a choice: attack or flee. If he attacks, we’ll set
strike to true. If he tries to flee and succeeds, we drop
the enemy strength and end the encounter. Otherwise, strike is false.
Then we create a random number for combat, adjusted for the relative strength of the
hero and slime. A slime’s power is in its health, once it’s chopped into little bits,
it’s not much of a threat. Whoever wins the roll gets to deal damage to the other,
except if Biff was trying to flee, strike is false so he doesn’t land a hit.
Changing the game state
We’d like it to be possible for the user to quit from within an encounter. Now, Tenet will
almost certainly add a builtin like C’s exit function, but it’d be cleaner if our logic
could handle a request to quit more naturally. We could simply set Biff’s hitpoints to 0, but
it seems like a kludge to kill off the player rather than simply have the game quit.
What we’ll do instead is return a union type of normal ~ Game for when the game should
continue normally, or #quit for when the game should quit.
// Hiding some functions we've already seen.
type Game = (hp:Int, xp:Int, enemy:Int);
let hero-factor = 5;
let enemy-factor = 3;
fun xp-to-level(xp: Int, factor: Int) => Int {
if xp lt 0 { return 0; }
return sqrt-int(xp / factor);
}
fun stat-level(game: Game) => Int {
return xp-to-level(game.xp, hero-factor);
}
// Determine the enemy's level.
fun stat-enemy-level(game: Game) => Int {
return xp-to-level(game.enemy, enemy-factor);
}
fun stat-bonk(game: Game) => Int {
return 3 * (stat-level(game) + 1);
}
// And hiding some functions we've yet to implement.
fun describe-slime(slime-hp: Int) {
print-line("You see a slime with a stubbed out description!");
}
fun award-xp(game: Game) => Game {
// Just award 10 each time.
let mut game' = game;
game'.xp += 10;
return game';
}
fun read-command(prompt: Str) => Str {
print-line(prompt);
return "f";
}
fun encounter(game: Game) => normal ~ Game | #quit {
// Set up the encounter.
let hero-attack = stat-bonk(game);
let mut slime-hp = stat-enemy-level(game) * 2 + random(5) - 2;
if slime-hp le 0 { slime-hp = 1; }
let mut game' = game;
// We loop until someone is dead.
while game'.hp gt 0 and slime-hp gt 0 {
describe-slime(slime-hp);
print-line("You have " ++ int-to-str(game'.hp) ++ " HP remaining.");
let act = read-command("a/f/h/q?");
// This is a flag indicating our character is trying to attack.
let mut strike = false;
when act {
"h" -> {
show-fight-help();
continue;
}
"q" -> return #quit;
"a" -> strike = true;
"f" -> {
if succeeds-fleeing(slime-hp) {
print-line("Brave Sir Biff ran away, bravely ran away!");
game'.enemy -= 2;
if game'.enemy lt 0 { game'.enemy = 0; }
return normal ~ game';
} else {
print-line("The slime blocks your retreat!");
}
}
_ -> {
print-line("Invalid command!");
continue; // Skip the next section entirely.
}
}
// Biff's attack is his attack skill, while the slime's is its HP.
let outcome = hero-attack - slime-hp + random(11) - 5;
if outcome gt 0 {
if strike {
let dmg = random(5) + 1;
print-line("You bonk the slime for " ++ int-to-str(dmg) ++ " points!");
slime-hp -= dmg;
}
} else if outcome lt 0 {
let dmg = random(5) + 1;
print-line("The slime sizzles you for " ++ int-to-str(dmg) ++ " points!");
game'.hp -= dmg;
} else if strike {
print-line("Neither you nor the slime harm the other.");
}
} // while
if game'.hp gt 0 {
print-line("You have defeated the slime!");
game' = award-xp(game');
}
return normal ~ game';
}
fun show-fight-help() {
print-line("While in combat, type 'help' for this help,");
print-line(" 'attack' to attempt to attack the slime,");
print-line(" 'flee' to try to run away from the slime,");
print-line(" 'quit' to quit the entire game,");
print-line("or just the first letter.");
}
fun succeeds-fleeing(slime-hp: Int) => Bool {
let mut chance = 95 - slime-hp;
if chance lt 25 { chance = 25; }
return random(100) lt chance;
}
do {
let before = (hp: 30, xp: 200, enemy: 100);
show("Before encounter: ", before);
when encounter(before) {
normal ~ let after -> show("After encounter: ", after);
#quit -> print-line("Game was quit.");
}
}
Names with hyphens and subtraction
The expression hero-attack - slime-hp shows that subtraction requires spaces
around the minus sign. The preferred style is to put spaces around all
arithmetic operators.
Last odds and ends
Let’s work out some functions we stubbed out. We’ll start with describe-slime as it’s pretty
easy. Our strategy is to simply divide the slime’s remaining HP by 5 and look up a description
in a list.
let slime-descriptions = \[
"You are pestered by a nasty little slime.",
"You are pestered by a small irksome slime.",
"You face a wretched slime.",
"You face a bubbling slime.",
"You face a large noisome slime.",
"A huge pulsating slime towers before you!",
"An enormous, disgusting slime towers over you!"
];
// The description of the slime is a hint as to how many
// hitpoints it has left. We're essentially squishing it
// until there's no more slime left.
fun describe-slime(slime-hp: Int) {
let size-category = slime-hp / 5;
if size-category in slime-descriptions {
print-line(slime-descriptions[size-category]);
} else {
print-line("A slime's inexplicable ichors fill the dungeon!");
}
}
do {
describe-slime(slime-hp: 1);
describe-slime(slime-hp: 7);
describe-slime(slime-hp: 34);
}
Working with lists
We declare slime-descriptions at the top level, outside of any function or do block.
Names declared at the top level must be immutable at this time.
As a regular list, we can index into it using the usual list[index] notation. Here,
though, we check size in slime-descriptions first: it tells us if the given index is
valid. If the index was invalid, the interpreter would panic.
Panics are Tenet’s mechanism for unrecoverable errors. The design of lists assumes that an algorithm only reads elements actually in the list, and an invalid index must be a bug. Use
into check your indices when your algorithm doesn’t make such guarantees.
Awarding experience
The last item before we present the game all put together is awarding experience when our hero defeats monsters.
type Game = (hp:Int, xp:Int, enemy:Int);
let hero-factor = 5;
let enemy-factor = 3;
fun xp-to-level(xp: Int, factor: Int) => Int {
if xp lt 0 { return 0; }
return sqrt-int(xp / factor);
}
fun stat-level(game: Game) => Int {
return xp-to-level(game.xp, hero-factor);
}
// Determine the enemy's level.
fun stat-enemy-level(game: Game) => Int {
return xp-to-level(game.enemy, enemy-factor);
}
do {
let mut g = (hp: 0, xp: 0, enemy: 10);
show("first award", g = award-xp(g));
show("second award", g = award-xp(g));
show("third award", g = award-xp(g));
}
// Award XP and note if the character has leveled up.
fun award-xp(game: Game) => Game {
let hero-level = stat-level(game);
let mut award = random(5) + stat-enemy-level(game) - hero-level;
if award lt 1 { award = 1; }
let mut game' = game;
game'.xp = stat-xp(game) + award;
if (let new-level = xp-to-level(game'.xp, hero-factor)) gt hero-level {
print-line("You have advanced to level " ++ int-to-str(new-level) ++ "!");
}
return game';
}
Inline assignment
Many languages can do inline assignment; Tenet takes it a bit further and allows
inline declarations. This uses the same syntax as capturing values with the when statement
or ? operator.
The whole game
Finally, putting it all together, here’s the entire game.
// Basic data reflecting game state.
type Game = (
hp: Int,
xp: Int,
enemy: Int
);
fun new-game() => Game {
// Set up the game state with default values.
let mut game: Game = (hp: 0, xp: 0, enemy: 0);
// Then set our character's HP to the max.
game.hp = stat-max-hp(game);
return game;
}
// Biff's XP/level scaling factor
let hero-factor = 5;
// Slimes get XP as you go deeper into the dungeon
let enemy-factor = 3;
// Given an amount of xp, determine the level of a thing.
fun xp-to-level(xp: Int, factor: Int) => Int {
if xp lt 0 { return 0; }
return sqrt-int(xp / factor);
}
// Given a level, determine how much XP is necessary to attain it.
fun level-to-xp(level: Int, factor: Int) => Int {
return level * level * factor;
}
// Get the current HP of our hero.
fun stat-hp(game: Game) => Int {
return game.hp;
}
// Get the current XP of our hero.
fun stat-xp(game: Game) => Int {
return game.xp;
}
// Get the level of our hero.
fun stat-level(game: Game) => Int {
return xp-to-level(game.xp, hero-factor);
}
// Determine the enemy's level.
fun stat-enemy-level(game: Game) => Int {
return xp-to-level(game.enemy, enemy-factor);
}
// Determine the attack strength of our hero.
fun stat-bonk(game: Game) => Int {
return 3 * (stat-level(game) + 1);
}
// Determine the max HP of our hero.
fun stat-max-hp(game: Game) => Int {
return 5 * (stat-level(game) + 1);
}
// Award XP and note if the character has leveled up.
fun award-xp(game: Game) => Game {
let hero-level = stat-level(game);
let mut award = random(5) + stat-enemy-level(game) - hero-level;
if award lt 1 { award = 1; }
let mut game' = game;
game'.xp = stat-xp(game) + award;
if (let new-level = xp-to-level(game'.xp, hero-factor)) gt hero-level {
print-line("You have advanced to level " ++ int-to-str(new-level) ++ "!");
}
return game';
}
// Restore some HP.
fun heal-hp(game: Game, max-heal: Int) => Game {
let hp = stat-hp(game);
let max-hp = stat-max-hp(game);
let mut new-hp = hp + random(bound: max-heal);
if new-hp gt max-hp {
new-hp = max-hp;
}
if new-hp gt hp {
print-line("You heal for " ++ int-to-str(new-hp - hp) ++ " HP");
}
let mut game' = game;
game'.hp = new-hp;
return game';
}
// Have our character venture deeper and fight slimes.
fun explore(game: Game) => normal ~ Game | #quit {
print-line("You venture deeper into the dark depths.");
let mut game' = game;
game'.enemy += 1;
if random(100) lt 75 {
print-line("You find a slime!");
return encounter(game');
} else {
print-line("The winding dungeon tunnels are eerily empty.");
return normal ~ game';
}
}
// Have our character pause to recover health, but possibly be found by a slime.
fun rest(game: Game) => normal ~ Game | #quit {
print-line("You rest.");
let mut game' = game;
if random(100) lt 25 {
game' = heal-hp(game', max-heal:5);
print-line("Your rest is interrupted by a squishing sound!");
return encounter(game');
} else {
game' = heal-hp(game', max-heal:15);
print-line("Thankfully, no slime finds you.");
return normal ~ game';
}
}
fun succeeds-fleeing(slime-hp: Int) => Bool {
let mut chance = 95 - slime-hp;
if chance lt 25 { chance = 25; }
return random(100) lt chance;
}
let slime-descriptions = \[
"You are pestered by a nasty little slime.",
"You are pestered by a small irksome slime.",
"You face a wretched slime.",
"You face a bubbling slime.",
"You face a large noisome slime.",
"A huge pulsating slime towers before you!",
"An enormous, disgusting slime towers over you!"
];
// The description of the slime is a hint as to how many
// hitpoints it has left. We're essentially squishing it
// until there's no more slime left.
fun describe-slime(slime-hp: Int) {
let size = slime-hp / 5;
if size in slime-descriptions {
print-line(slime-descriptions[size]);
} else {
print-line("A slime's inexplicable ichors fill the dungeon!");
}
}
fun encounter(game: Game) => normal ~ Game | #quit {
let hero-attack = stat-bonk(game);
let mut slime-hp = stat-enemy-level(game) * 2 + random(5) - 2;
if slime-hp le 0 { slime-hp = 1; }
let mut game' = game;
while game'.hp gt 0 and slime-hp gt 0 {
describe-slime(slime-hp);
print-line("You have " ++ int-to-str(game'.hp) ++ " HP remaining.");
let act = read-command("a/f/h/q?");
// Flag indicating our character is trying to attack.
let mut strike = false;
when act {
"h" -> {
show-fight-help();
continue;
}
"q" -> return #quit;
"a" -> strike = true;
"f" -> {
if succeeds-fleeing(slime-hp) {
print-line("Brave Sir Biff ran away, bravely ran away!");
game'.enemy -= 2;
if game'.enemy lt 0 { game'.enemy = 0; }
return normal ~ game';
} else {
print-line("The slime blocks your retreat!");
}
}
_ -> {
print-line("Invalid command!");
continue;
}
}
// Biff's attack is his attack skill, while
// the slime's is its HP.
let outcome = hero-attack - slime-hp + random(11) - 5;
if outcome gt 0 {
if strike {
let dmg = random(5) + 1;
print-line("You bonk the slime for " ++ int-to-str(dmg) ++ " points!");
slime-hp -= dmg;
}
} else if outcome lt 0 {
let dmg = random(5) + 1;
print-line("The slime sizzles you for " ++ int-to-str(dmg) ++ " points!");
game'.hp -= dmg;
} else if strike {
print-line("Neither you nor the slime harm the other.");
}
} // while
if game'.hp gt 0 {
print-line("You have defeated the slime!");
game' = award-xp(game');
}
return normal ~ game';
}
// Main loop controlling the game.
fun game-loop() => #quit {
let mut game = new-game();
show-help();
while true {
when read-command("e/r/s/h/q?") {
"e" -> game = explore(game) ! normal;
"r" -> game = rest(game) ! normal;
"s" -> show-stats(game);
"h" -> show-help();
"q" -> esc #quit;
_ -> print-line("Invalid command");
}
if game.hp le 0 {
print-line("Alas, you have fallen.");
esc #quit;
}
}
}
fun show-stats(game: Game) {
let hp = int-to-str(stat-hp(game));
let max-hp = int-to-str(stat-max-hp(game));
let xp = int-to-str(stat-xp(game));
let bonk = int-to-str(stat-bonk(game));
let level' = stat-level(game);
let level = int-to-str(level');
let xp-advance = int-to-str(level-to-xp(level' + 1, hero-factor));
let title = level' ? {
0 -> "Tiny";
1 -> "Scrawny";
2 -> "Okay";
_ -> "Mighty";
};
print-line("Name: Biff the " ++ title);
print-line("======================");
print-line("HP: " ++ hp ++ "/" ++ max-hp ++ " Level: " ++ level);
print-line("XP: " ++ xp ++ "/" ++ xp-advance);
print-line("");
print-line("Skills");
print-line("------");
print-line("Bonk: " ++ bonk ++ " Stratigraphy: 13");
}
fun show-help() {
print-line("Welcome to the Gruesome Caverns.");
print-line("");
print-line("While exploring, type 'help' for this help,");
print-line(" 'explore' to explore deeper,");
print-line(" 'rest' to risk resting,");
print-line(" 'stats' to see your stats,");
print-line(" 'quit' to quit,");
print-line("or just the first letter.");
print-line("");
print-line("In a fight, type 'attack' or 'flee'.");
}
fun show-fight-help() {
print-line("While in combat, type 'help' for this help,");
print-line(" 'attack' to attempt to attack the slime,");
print-line(" 'flee' to try to run away from the slime,");
print-line(" 'quit' to quit the entire game,");
print-line("or just the first letter.");
}
fun read-command(prompt: Str) => Str {
// The read-line builtin displays a prompt and
// reads a line of text the user enters.
return read-line(prompt) ? {
#eof -> "q";
line ~ "" -> "";
// As mentioned above, we only care about
// a single letter.
line ~ let cmd -> lowercase(cmd[0]);
};
}
do {
game-loop();
}