Seance
Description of the page content
Conversion of Seance to several programming languages.
This program has been converted to 12 programming languages.
Original
Origin of seance.bas:
By Chris Oxlade, 1983.
Book: Creepy Computer Games (Usborne Publishing Ltd.)
- https://archive.org/details/seance.qb64
- https://github.com/chaosotter/basic-games
5 RANDOMIZE TIMER
6 GOSUB 5000
10 LET S = 0
20 LET G = 0
30 LET CS = 64
40 CLS
60 PRINT TAB(34); "** Seance **": PRINT
70 COLOR 11: FOR I = 1 TO 8
80 LET X = 6 + I
90 LET Y = 5
100 LET A$ = CHR$(CS + I)
110 GOSUB 710
120 LET Y = 11
130 LET A$ = CHR$(CS + 22 - I)
140 GOSUB 710
150 NEXT I
160 FOR I = 1 TO 5
170 LET X = 5
180 LET Y = 5 + I
190 LET A$ = CHR$(CS + 27 - I)
200 GOSUB 710
210 LET X = 16
220 LET A$ = CHR$(CS + 8 + I)
230 GOSUB 710
240 NEXT I
250 LET P$ = ""
260 LET N = INT(RND(1) * 4 + 3)
270 FOR I = 1 TO N
280 LET A$ = "*"
290 LET L = INT(RND(1) * 26 + 1)
300 LET S$ = CHR$(CS + L)
310 LET P$ = P$ + S$
320 LET D = 4
330 IF L < 22 THEN LET D = 3
340 IF L < 14 THEN LET D = 2
350 IF L < 9 THEN LET D = 1
360 ON D GOTO 370, 400, 430, 460
370 LET Y = 6
380 LET X = L + 6
390 GOTO 480
400 LET X = 15
410 LET Y = L - 3
420 GOTO 480
430 LET Y = 10
440 LET X = 28 - L
450 GOTO 480
460 LET X = 6
470 LET Y = 32 - L
480 COLOR 14: GOSUB 710: COLOR 15
490 ST = TIMER
500 IF TIMER <= ST + 1 THEN 500
510 LET A$ = " "
520 GOSUB 710
530 NEXT I
540 LET A$ = ""
550 LET X = 0
560 LET Y = 13
570 GOSUB 710
580 LOCATE 16, 35: COLOR 10: INPUT R$: COLOR 15
585 RR$ = "": FOR R = 1 TO LEN(R$): A$ = MID$(R$, R, 1)
586 IF A$ >= "a" AND A$ <= "z" THEN RR$ = RR$ + CHR$(ASC(A$) - 32) ELSE RR$ = RR$ + A$
587 NEXT R: R$ = RR$
590 IF R$ = P$ THEN GOTO 670
600 LET G = G + 1
610 IF G = 1 THEN LOCATE 20, 25: COLOR 12: PRINT "The table begins to shake!": COLOR 15
620 IF G = 2 THEN LOCATE 20, 25: COLOR 12: PRINT "The light bulb shatters!": COLOR 15
630 IF G = 3 THEN GOTO 730
640 IF INKEY$ = "" THEN 640
660 GOTO 40
670 LET S = S + N
680 IF S < 50 THEN GOTO 40
690 PRINT: PRINT "Whew! The spirits have gone!"
695 PRINT "You live to face another day!"
700 END
710 LOCATE Y + 1, X + 29: PRINT A$;
720 RETURN
730 PRINT: PRINT "OH NO! A pair of clammy hands grasps your neck!"
735 PRINT "You die..."
740 END
5000 CLS: COLOR 12
5010 PRINT TAB(37); "Seance": PRINT: PRINT
5020 COLOR 14: PRINT
5030 PRINT "Messages from the Spirits are coming through, letter by letter. They want you"
5040 PRINT "to remember the letters and type them into the computer in the correct order."
5050 PRINT "If you make mistakes, they will be angry -- very angry..."
5060 PRINT
5070 PRINT "Watch for stars on your screen -- they show the letters in the Spirits'"
5080 PRINT "messages."
5090 PRINT
5100 COLOR 13: PRINT "(Press any key.)"
5110 IF INKEY$ = "" THEN 5110
5120 CLS: COLOR 15: RETURN
In C#
// Seance
// Original version in BASIC:
// By Chris Oxlade, 1983.
// Creepy Computer Games (Usborne Publishing Ltd.)
// - https://archive.org/details/seance.qb64
// - https://github.com/chaosotter/basic-games
// This version in C#:
// Copyright (c) 2024, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2024-12-25.
//
// Last modified: 20251205T1547+0100.
using System;
using System.Threading;
class Seance
{
const int MAX_SCORE = 50;
const int MAX_MESSAGE_LENGTH = 6;
const int MIN_MESSAGE_LENGTH = 3;
const int BASE_CHARACTER_CODE = (int) '@';
const char PLANCHETTE = '*';
const int FIRST_LETTER_NUMBER = 1;
const int LAST_LETTER_NUMBER = 26;
const ConsoleColor BOARD_COLOR = ConsoleColor.Cyan;
const ConsoleColor DEFAULT_COLOR = ConsoleColor.White;
const ConsoleColor INPUT_COLOR = ConsoleColor.Green;
const ConsoleColor INSTRUCTIONS_COLOR = ConsoleColor.DarkYellow;
const ConsoleColor MISTAKE_EFFECT_COLOR = ConsoleColor.Red;
const ConsoleColor PLANCHETTE_COLOR = ConsoleColor.DarkYellow;
const ConsoleColor TITLE_COLOR = ConsoleColor.Red;
const int INPUT_X = BOARD_X;
const int INPUT_Y = BOARD_Y + BOARD_BOTTOM_Y + 4;
const int MESSAGES_Y = INPUT_Y;
const int MISTAKE_EFFECT_PAUSE = 3; // seconds
const int BOARD_ACTUAL_WIDTH = BOARD_WIDTH + 2 * BOARD_PAD; // screen columns
const int BOARD_BOTTOM_Y = BOARD_HEIGHT + 1; // relative to the board
const int BOARD_HEIGHT = 5; // characters displayed on the left and right borders
const int BOARD_PAD = 1; // blank characters separating the board from its left and right borders
const int BOARD_WIDTH = 8; // characters displayed on the top and bottom borders
const int BOARD_X = 29; // screen column
const int BOARD_Y = 5; // screen line
// Print the given prompt and wait until the user enters a string.
//
static string Input(string prompt = "")
{
Console.ForegroundColor = INPUT_COLOR;
Console.Write(prompt);
string result = Console.ReadLine();
Console.ForegroundColor = DEFAULT_COLOR;
return result;
}
// Print the given prompt and wait until the user presses Enter.
//
static void PressEnter(string prompt)
{
Input(prompt);
}
// Return the x coordinate to print the given text centered on the board.
//
static int boardCenteredX(string text)
{
return BOARD_X + (BOARD_ACTUAL_WIDTH - text.Length) / 2;
}
// Print the given text on the given row, centered on the board.
//
static void PrintBoardCentered(string text, int y)
{
Console.SetCursorPosition(boardCenteredX(text), y);
Console.WriteLine(text);
}
const string TITLE = "Seance";
// Print the title at the current cursor position.
//
static void PrintTitle()
{
Console.ForegroundColor = TITLE_COLOR;
Console.WriteLine(TITLE);
Console.ForegroundColor = DEFAULT_COLOR;
}
// Print the title on the given row, centered on the board.
//
static void PrintBoardCenteredTitle(int y)
{
Console.ForegroundColor = TITLE_COLOR;
PrintBoardCentered(TITLE, y);
Console.ForegroundColor = DEFAULT_COLOR;
}
static void PrintCredits()
{
PrintTitle();
Console.WriteLine("\nOriginal version in BASIC:");
Console.WriteLine(" Written by Chris Oxlade, 1983.");
Console.WriteLine(" https://archive.org/details/seance.qb64");
Console.WriteLine(" https://github.com/chaosotter/basic-games");
Console.WriteLine("\nThis version in C#:");
Console.WriteLine(" Copyright (c) 2024, Marcos Cruz (programandala.net)");
Console.WriteLine(" SPDX-License-Identifier: Fair");
}
static void PrintInstructions()
{
PrintTitle();
Console.ForegroundColor = INSTRUCTIONS_COLOR;
Console.WriteLine("\nMessages from the spirits are coming through, letter by letter. They want you");
Console.WriteLine("to remember the letters and type them into the computer in the correct order.");
Console.WriteLine("If you make mistakes, they will be angry -- very angry...");
Console.WriteLine();
Console.WriteLine("Watch for stars on your screen -- they show the letters in the spirits'");
Console.WriteLine("messages.");
Console.ForegroundColor = DEFAULT_COLOR;
}
// Print the given letter at the given board coordinates.
//
static void PrintCharacter(int y, int x, char a)
{
Console.SetCursorPosition(x + BOARD_X, y + BOARD_Y);
Console.Write(a);
}
static void PrintBoard()
{
Console.ForegroundColor = BOARD_COLOR;
for (int i = 1; i <= BOARD_WIDTH; i++)
{
PrintCharacter(0, i + 1, (char) (BASE_CHARACTER_CODE + i)); // top border
PrintCharacter(BOARD_BOTTOM_Y, i + 1, (char) (BASE_CHARACTER_CODE + LAST_LETTER_NUMBER - BOARD_HEIGHT - i + 1)); // bottom border
}
for (int i = 1; i <= BOARD_HEIGHT; i++)
{
PrintCharacter(i , 0, (char) (BASE_CHARACTER_CODE + LAST_LETTER_NUMBER - i + 1)); // left border
PrintCharacter(i , 3 + BOARD_WIDTH, (char) (BASE_CHARACTER_CODE + BOARD_WIDTH + i)); // right border
}
Console.WriteLine();
Console.ForegroundColor = DEFAULT_COLOR;
}
// Return a random integer in the given inclusive range.
//
static int RandomInInclusiveRange(int min, int max)
{
Random rand = new Random();
return rand.Next(max - min + 1) + min;
}
// Print the given mistake effect, wait a configured number of seconds and
// finally erase it.
//
static void PrintMistakeEffect(string effect)
{
int x = boardCenteredX(effect);
Console.CursorVisible = false;
Console.SetCursorPosition(x, MESSAGES_Y);
Console.ForegroundColor = MISTAKE_EFFECT_COLOR;
Console.WriteLine(effect);
Thread.Sleep(1000 * MISTAKE_EFFECT_PAUSE); // milliseconds
Console.ForegroundColor = DEFAULT_COLOR;
Console.SetCursorPosition(x, MESSAGES_Y);
string spaces = new string(' ', effect.Length);
Console.Write(spaces);
Console.CursorVisible = true;
}
// Return a new message of the given length, after marking its letters on the
// board.
//
static string Message(int length)
{
int y;
int x;
string message = "";
Console.CursorVisible = false;
for (int i = 1; i <= length; i++)
{
int letterNumber = RandomInInclusiveRange(FIRST_LETTER_NUMBER, LAST_LETTER_NUMBER);
char letter = (char) (BASE_CHARACTER_CODE + letterNumber);
message = $"{message}{letter}";
if (letterNumber <= BOARD_WIDTH)
{
// top border
y = 1;
x = letterNumber + 1;
}
else if (letterNumber <= BOARD_WIDTH + BOARD_HEIGHT)
{
// right border
y = letterNumber - BOARD_WIDTH;
x = 2 + BOARD_WIDTH;
}
else if (letterNumber <= BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH)
{
// bottom border
y = BOARD_BOTTOM_Y - 1;
x = 2 + BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH - letterNumber;
}
else
{
// left border
y = 1 + LAST_LETTER_NUMBER - letterNumber;
x = 1;
}
Console.ForegroundColor = PLANCHETTE_COLOR;
PrintCharacter(y, x, PLANCHETTE);
Console.ForegroundColor = DEFAULT_COLOR;
Thread.Sleep(1000); // milliseconds
PrintCharacter(y, x, ' ');
}
Console.CursorVisible = true;
return message;
}
// Accept a string from the user, erase it from the screen and return it.
//
static string MessageUnderstood()
{
string prompt = "? ";
Console.SetCursorPosition(INPUT_X, INPUT_Y);
string message = Input(prompt).ToUpper();
string spaces = new string(' ', prompt.Length + message.Length);
Console.SetCursorPosition(INPUT_X, INPUT_Y);
Console.Write(spaces);
return message;
}
static void Play()
{
int score = 0;
int mistakes = 0;
PrintBoardCenteredTitle(1);
PrintBoard();
while (true)
{
int messageLength = RandomInInclusiveRange(MIN_MESSAGE_LENGTH, MAX_MESSAGE_LENGTH);
if (Message(messageLength) != MessageUnderstood())
{
mistakes++;
switch (mistakes)
{
case 1:
PrintMistakeEffect("The table begins to shake!");
break;
case 2:
PrintMistakeEffect("The light bulb shatters!");
break;
case 3:
PrintMistakeEffect("Oh, no! A pair of clammy hands grasps your neck!");
return;
}
}
else
{
score += messageLength;
if (score >= MAX_SCORE)
{
PrintBoardCentered("Whew! The spirits have gone!", MESSAGES_Y);
PrintBoardCentered("You live to face another day!", MESSAGES_Y + 1);
return;
}
}
}
}
static void Main()
{
Console.ForegroundColor = DEFAULT_COLOR;
Console.Clear();
PrintCredits();
PressEnter("\nPress the Enter key to read the instructions. ");
Console.Clear();
PrintInstructions();
PressEnter("\nPress the Enter key to start. ");
Console.Clear();
Play();
Console.WriteLine("\n");
}
}
In Chapel
// Seance
// Original version in BASIC:
// By Chris Oxlade, 1983.
// https://archive.org/details/seance.qb64
// https://github.com/chaosotter/basic-games
// This version in Chapel:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2025-04-09.
//
// Last modified: 20250731T1954+0200.
// Modules {{{1
// =============================================================================
import IO;
import Random;
import Time;
// Terminal {{{1
// =============================================================================
const BLACK = 0;
const RED = 1;
const GREEN = 2;
const YELLOW = 3;
const BLUE = 4;
const MAGENTA = 5;
const CYAN = 6;
const WHITE = 7;
const DEFAULT = 9;
const STYLE_OFF = 20;
const FOREGROUND = 30;
const BACKGROUND = 40;
const BRIGHT = 60;
const NORMAL_STYLE = 0;
proc moveCursorHome() {
write("\x1B[H");
}
proc setCursorPosition(y: int, x: int) {
writef("\x1B[%i;%iH", y, x);
}
proc hideCursor() {
write("\x1B[?25l");
}
proc showCursor() {
write("\x1B[?25h");
}
proc setStyle(style: int) {
writef("\x1B[%im", style);
}
proc resetAttributes() {
setStyle(NORMAL_STYLE);
}
proc eraseLineToEnd() {
write("\x1B[K");
}
proc eraseScreen() {
write("\x1B[2J");
}
proc clearScreen() {
eraseScreen();
resetAttributes();
moveCursorHome();
}
// Config {{{1
// =============================================================================
const TITLE = "Seance";
const MAX_SCORE = 50;
const MAX_MESSAGE_LENGTH = 6;
const MIN_MESSAGE_LENGTH = 3;
const BASE_CHARACTER = '@'.toCodepoint();
const PLANCHETTE = '*'.toCodepoint();
const SPACE = ' '.toCodepoint();
const FIRST_LETTER_NUMBER = 1;
const LAST_LETTER_NUMBER = 26;
const BOARD_INK = BRIGHT + CYAN + FOREGROUND;
const DEFAULT_INK = WHITE + FOREGROUND;
const INPUT_INK = BRIGHT + GREEN + FOREGROUND;
const INSTRUCTIONS_INK = YELLOW + FOREGROUND;
const MISTAKE_EFFECT_INK = BRIGHT + RED + FOREGROUND;
const PLANCHETTE_INK = YELLOW + FOREGROUND;
const TITLE_INK = BRIGHT + RED + FOREGROUND;
const BOARD_X = 29; // screen column
const BOARD_Y = 5; // screen line
const BOARD_HEIGHT = 5; // characters displayed on the left and right borders
const BOARD_WIDTH = 8; // characters displayed on the top and bottom borders
const BOARD_PAD = 1; // blank characters separating the board from its left and right borders
const BOARD_ACTUAL_WIDTH = BOARD_WIDTH + 2 * BOARD_PAD; // screen columns
const BOARD_BOTTOM_Y = BOARD_HEIGHT + 1; // relative to the board
const INPUT_X = BOARD_X;
const INPUT_Y = BOARD_Y + BOARD_BOTTOM_Y + 4;
const MESSAGES_Y = INPUT_Y;
const MISTAKE_EFFECT_PAUSE = 3.0; // seconds
// User input {{{1
// =============================================================================
proc acceptString(prompt: string): string {
write(prompt);
IO.stdout.flush();
return IO.readLine().strip();
}
proc pressEnter(prompt: string) {
acceptString(prompt);
}
// Credits and instructions {{{1
// =============================================================================
proc printTitle() {
setStyle(TITLE_INK);
writef("%s\n", TITLE);
setStyle(DEFAULT_INK);
}
proc printCredits() {
printTitle();
writeln("\nOriginal version in BASIC:");
writeln(" Written by Chris Oxlade, 1983.");
writeln(" https://archive.org/details/seance.qb64");
writeln(" https://github.com/chaosotter/basic-games");
writeln("This version in Chapel:");
writeln(" Copyright (c) 2025, Marcos Cruz (programandala.net)");
writeln(" SPDX-License-Identifier: Fair");
}
proc printInstructions() {
printTitle();
setStyle(INSTRUCTIONS_INK);
writeln("\nMessages from the Spirits are coming through, letter by letter. They want you");
writeln("to remember the letters and type them into the computer in the correct order.");
writeln("If you make mistakes, they will be angry -- very angry...");
writeln("");
writeln("Watch for stars on your screen -- they show the letters in the Spirits'");
writeln("messages.");
setStyle(DEFAULT_INK);
}
// Game {{{1
// =============================================================================
var rsInt = new Random.randomStream(int);
// Return the x coordinate to print the given text centered on the board.
//
proc boardCenteredX(text: string): int {
return (BOARD_X + (BOARD_ACTUAL_WIDTH - text.size) / 2): int;
}
// Print the given text on the given row, centered on the board.
//
proc printCentered(text: string, y: int) {
setCursorPosition(y, boardCenteredX(text));
writef("%s\n", text);
}
// Print the title on the given row, centered on the board.
//
proc printCenteredTitle(y: int) {
setStyle(TITLE_INK);
printCentered(TITLE, y);
setStyle(DEFAULT_INK);
}
// Print the given letter at the given board coordinates.
//
proc printCharacter(y: int, x: int, charCode: int) {
setCursorPosition(y + BOARD_Y, x + BOARD_X);
write(codepointToString(charCode: int(32)));
}
proc printBoard() {
setStyle(BOARD_INK);
for i in 1 .. BOARD_WIDTH {
printCharacter(0, i + 1, BASE_CHARACTER + i); // top border
printCharacter(BOARD_BOTTOM_Y, i + 1, BASE_CHARACTER + LAST_LETTER_NUMBER - BOARD_HEIGHT - i + 1); // bottom border
}
for i in 1 .. BOARD_HEIGHT {
printCharacter(i , 0, BASE_CHARACTER + LAST_LETTER_NUMBER - i + 1); // left border
printCharacter(i , 3 + BOARD_WIDTH, BASE_CHARACTER + BOARD_WIDTH + i); // right border
}
writeln("");
setStyle(DEFAULT_INK);
}
proc eraseLineFrom(line: int, column: int) {
setCursorPosition(line, column);
eraseLineToEnd();
}
// Print the given mistake effect, do a pause and erase it.
//
proc printMistakeEffect(effect: string) {
var x: int = boardCenteredX(effect);
hideCursor();
setCursorPosition(MESSAGES_Y, x);
setStyle(MISTAKE_EFFECT_INK);
writeln(effect);
setStyle(DEFAULT_INK);
Time.sleep(MISTAKE_EFFECT_PAUSE);
eraseLineFrom(MESSAGES_Y, x);
showCursor();
}
// Return a new message of the given length, after marking its letters on the
// board.
//
proc message(length: int): string {
const LETTER_PAUSE: real = 1.0; // seconds
var y: int = 0;
var x: int = 0;
var letters: string;
hideCursor();
for i in 0 .. length {
var letterNumber: int = rsInt.next(
FIRST_LETTER_NUMBER,
LAST_LETTER_NUMBER
);
letters += codepointToString((BASE_CHARACTER + letterNumber): int(32));
if letterNumber <= BOARD_WIDTH {
// top border
y = 1;
x = letterNumber + 1;
} else if letterNumber <= BOARD_WIDTH + BOARD_HEIGHT {
// right border
y = letterNumber - BOARD_WIDTH;
x = 2 + BOARD_WIDTH;
} else if letterNumber <= BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH {
// bottom border
y = BOARD_BOTTOM_Y - 1;
x = 2 + BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH - letterNumber;
} else {
// left border
y = 1 + LAST_LETTER_NUMBER - letterNumber;
x = 1;
}
setStyle(PLANCHETTE_INK);
printCharacter(y, x, PLANCHETTE);
writeln();
Time.sleep(LETTER_PAUSE);
setStyle(DEFAULT_INK);
printCharacter(y, x, SPACE);
}
showCursor();
return letters;
}
proc acceptMessage(): string {
setStyle(INPUT_INK);
setCursorPosition(INPUT_Y, INPUT_X);
var result: string = acceptString("? ").toUpper();
setStyle(DEFAULT_INK);
eraseLineFrom(INPUT_Y, INPUT_X);
return result;
}
proc play() {
var score: int = 0;
var mistakes: int = 0;
printCenteredTitle(1);
printBoard();
while true {
var messageLength: int = rsInt.next(
MIN_MESSAGE_LENGTH,
MAX_MESSAGE_LENGTH
);
var messageReceived: string = message(messageLength);
var messageUnderstood: string = acceptMessage();
if messageReceived != messageUnderstood {
mistakes += 1;
select mistakes {
when 1 {
printMistakeEffect("The table begins to shake!");
}
when 2 {
printMistakeEffect("The light bulb shatters!");
}
when 3 {
printMistakeEffect("Oh, no! A pair of clammy hands grasps your neck!");
return;
}
}
} else {
score += messageLength;
if score >= MAX_SCORE {
printCentered("Whew! The spirits have gone!", MESSAGES_Y);
printCentered("You live to face another day!", MESSAGES_Y + 1);
return;
}
}
}
}
// Main {{{1
// =============================================================================
proc main() {
setStyle(DEFAULT_INK);
clearScreen();
printCredits();
pressEnter("\nPress the Enter key to read the instructions. ");
clearScreen();
printInstructions();
pressEnter("\nPress the Enter key to start. ");
clearScreen();
play();
writeln("");
}
In Crystal
# Seance
# Original version in BASIC:
# By Chris Oxlade, 1983.
# https://archive.org/details/seance.qb64
# https://github.com/chaosotter/basic-games
# This version in Crystal:
# Copyright (c) 2024, Marcos Cruz (programandala.net)
# SPDX-License-Identifier: Fair
#
# Written on 2024-11-27.
#
# Last modified: 20250421T0020+0200.
# Terminal {{{1
# ==============================================================================
# Screen colors
BLACK = 0
RED = 1
GREEN = 2
YELLOW = 3
BLUE = 4
MAGENTA = 5
CYAN = 6
WHITE = 7
DEFAULT = 9
# Screen attributes
NORMAL = 0
# Screen color offsets
FOREGROUND = +30
BRIGHT = +60
# Moves the cursor to the home position.
def home
print "\e[H"
end
# Clears the screen and moves the cursor to the home position.
def clear_screen
print "\e[2J"
home
end
def set_color(color : Int)
print "\e[#{color}m"
end
def set_attribute(attr : Int)
print "\e[0;#{attr}m"
end
# Sets the cursor position to the given coordinates (the top left position is 1, 1).
def set_cursor_position(line, column : Int)
print "\e[#{line};#{column}H"
end
def erase_line_right
print "\e[K"
end
def hide_cursor
print "\e[?25l"
end
def show_cursor
print "\e[?25h"
end
# Input {{{1
# ==============================================================================
# Prints the given prompt and waits until the user enters a string.
def input(prompt = "") : String
s = nil
set_color(INPUT_INK)
while s.is_a?(Nil)
print prompt
s = gets
end
set_color(DEFAULT_INK)
s
end
# Prints the given prompt and waits until the user presses Enter.
def press_enter(prompt : String)
input(prompt)
end
# Globals {{{1
# ==============================================================================
MAX_SCORE = 50
MAX_MESSAGE_LENGTH = 6
MIN_MESSAGE_LENGTH = 3
BASE_CHARACTER = '@'.ord
PLANCHETTE = '*'
FIRST_LETTER_NUMBER = 1
LAST_LETTER_NUMBER = 26
BOARD_INK = FOREGROUND + BRIGHT + CYAN
DEFAULT_INK = FOREGROUND + WHITE
INPUT_INK = FOREGROUND + BRIGHT + GREEN
INSTRUCTIONS_INK = FOREGROUND + YELLOW
MISTAKE_EFFECT_INK = FOREGROUND + BRIGHT + RED
PLANCHETTE_INK = FOREGROUND + YELLOW
TITLE_INK = FOREGROUND + BRIGHT + RED
INPUT_X = BOARD_X
INPUT_Y = BOARD_Y + BOARD_BOTTOM_Y + 4
MESSAGES_Y = INPUT_Y
MISTAKE_EFFECT_PAUSE = 3 # seconds
BOARD_ACTUAL_WIDTH = BOARD_WIDTH + 2 * BOARD_PAD # screen columns
BOARD_BOTTOM_Y = BOARD_HEIGHT + 1 # relative to the board
BOARD_HEIGHT = 5 # characters displayed on the left and right borders
BOARD_PAD = 1 # blank characters separating the board from its left and right borders
BOARD_WIDTH = 8 # characters displayed on the top and bottom borders
BOARD_X = 29 # screen column
BOARD_Y = 5 # screen line
TITLE = "Seance"
# Output {{{1
# ==============================================================================
# Returns the x coordinate to print the given text centered on the board.
def board_centered_x(text : String) : Int
return (BOARD_X + (BOARD_ACTUAL_WIDTH - text.size) // 2)
end
# Prints the given text on the given row, centered on the board.
def print_board_centered(text : String, y : Int)
set_cursor_position(y, board_centered_x(text))
puts text
end
# Prints the title at the current cursor position.
def print_title
set_color(TITLE_INK)
puts TITLE
set_color(DEFAULT_INK)
end
# Prints the title on the given row, centered on the board.
def print_board_centered_title(y : Int)
set_color(TITLE_INK)
print_board_centered(TITLE, y)
set_color(DEFAULT_INK)
end
# Information {{{1
# ==============================================================================
def print_credits
print_title
puts
puts "Original version in BASIC:"
puts " Written by Chris Oxlade, 1983."
puts " https://archive.org/details/seance.qb64"
puts " https://github.com/chaosotter/basic-games"
puts
puts "This version in Crystal:"
puts " Copyright (c) 2024, Marcos Cruz (programandala.net)"
puts " SPDX-License-Identifier: Fair"
end
def print_instructions
print_title
set_color(INSTRUCTIONS_INK)
puts "\nMessages from the Spirits are coming through, letter by letter. They want you"
puts "to remember the letters and type them into the computer in the correct order."
puts "If you make mistakes, they will be angry ― very angry…"
puts
puts "Watch for stars on your screen ― they show the letters in the Spirits'"
puts "messages."
set_color(DEFAULT_INK)
end
# Board {{{1
# ==============================================================================
# Prints the given letter at the given board coordinates.
def print_character(y, x : Int, a : Char)
set_cursor_position(y + BOARD_Y, x + BOARD_X)
print a
end
def print_board
set_color(BOARD_INK)
(1..BOARD_WIDTH).each do |i|
print_character(0, i + 1, (BASE_CHARACTER + i).chr) # top border
print_character(BOARD_BOTTOM_Y, i + 1, (BASE_CHARACTER + LAST_LETTER_NUMBER - BOARD_HEIGHT - i + 1).chr) # bottom border
end
(1..BOARD_HEIGHT).each do |i|
print_character(i, 0, (BASE_CHARACTER + LAST_LETTER_NUMBER - i + 1).chr) # left border
print_character(i, 3 + BOARD_WIDTH, (BASE_CHARACTER + BOARD_WIDTH + i).chr) # right border
end
puts
set_color(DEFAULT_INK)
end
# Erases the given line to the right of the given column.
def erase_line_right_from(line, column : Int)
set_cursor_position(line, column)
erase_line_right
end
# Prints the given mistake effect, waits a configured number of seconds and finally erases it.
def print_mistake_effect(effect : String)
x = board_centered_x(effect)
hide_cursor
set_cursor_position(MESSAGES_Y, x)
set_color(MISTAKE_EFFECT_INK)
print effect, "\n"
set_color(DEFAULT_INK)
sleep MISTAKE_EFFECT_PAUSE.seconds
erase_line_right_from(MESSAGES_Y, x)
show_cursor
end
# Returns a new message of the given length, after marking its letters on the board.
def message(length : Int) : String
y = 0
x = 0
message = ""
hide_cursor
(1..length).each do |i|
letter_number = rand(FIRST_LETTER_NUMBER..LAST_LETTER_NUMBER)
letter = (BASE_CHARACTER + letter_number).chr
message = "#{message}#{letter}"
case
when letter_number <= BOARD_WIDTH
# top border
y = 1
x = letter_number + 1
when letter_number <= BOARD_WIDTH + BOARD_HEIGHT
# right border
y = letter_number - BOARD_WIDTH
x = 2 + BOARD_WIDTH
when letter_number <= BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH
# bottom border
y = BOARD_BOTTOM_Y - 1
x = 2 + BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH - letter_number
else
# left border
y = 1 + LAST_LETTER_NUMBER - letter_number
x = 1
end
set_color(PLANCHETTE_INK)
print_character(y, x, PLANCHETTE)
set_color(DEFAULT_INK)
sleep 1.seconds
print_character(y, x, ' ')
end
show_cursor
return message
end
# Main {{{1
# ==============================================================================
# Accepts a string from the user, erases it from the screen and returns it.
def message_understood : String
set_cursor_position(INPUT_Y, INPUT_X)
erase_line_right_from(INPUT_Y, INPUT_X)
return input("? ").upcase
end
def play
score = 0
mistakes = 0
print_board_centered_title(1)
print_board
while true
message_length = rand(MIN_MESSAGE_LENGTH..MAX_MESSAGE_LENGTH)
if message(message_length) != message_understood
mistakes += 1
case mistakes
when 1
print_mistake_effect("The table begins to shake!")
when 2
print_mistake_effect("The light bulb shatters!")
when 3
print_mistake_effect("Oh, no! A pair of clammy hands grasps your neck!")
return
end
else
score += message_length
if score >= MAX_SCORE
print_board_centered("Whew! The spirits have gone!", MESSAGES_Y)
print_board_centered("You live to face another day!", MESSAGES_Y + 1)
return
end
end
end
end
set_color(DEFAULT_INK)
clear_screen
print_credits
press_enter("\nPress the Enter key to read the instructions. ")
clear_screen
print_instructions
press_enter("\nPress the Enter key to start. ")
clear_screen
play
puts
puts
In D
// Seance
// Original version in BASIC:
// By Chris Oxlade, 1983.
// https://archive.org/details/seance.qb64
// https://github.com/chaosotter/basic-games
// This version in D:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2025-03-24.
//
// Last modified: 20251220T0654+0100.
module seance;
// Terminal {{{1
// =============================================================================
enum BLACK = 0;
enum RED = 1;
enum GREEN = 2;
enum YELLOW = 3;
enum BLUE = 4;
enum MAGENTA = 5;
enum CYAN = 6;
enum WHITE = 7;
enum DEFAULT = 9;
enum STYLE_OFF = 20;
enum FOREGROUND = 30;
enum BACKGROUND = 40;
enum BRIGHT = 60;
enum NORMAL_STYLE = 0;
void moveCursorHome()
{
import std.stdio : write;
write("\x1B[H");
}
void setCursorPosition(int y, int x)
{
import std.stdio : writef;
writef("\x1B[%d;%dH", y, x);
}
void hideCursor()
{
import std.stdio : write;
write("\x1B[?25l");
}
void showCursor()
{
import std.stdio : write;
write("\x1B[?25h");
}
void setStyle(int style)
{
import std.stdio : writef;
writef("\x1B[%dm", style);
}
void resetAttributes()
{
setStyle(NORMAL_STYLE);
}
void eraseLineToEnd()
{
import std.stdio : write;
write("\x1B[K");
}
void eraseScreen()
{
import std.stdio : write;
write("\x1B[2J");
}
void clearScreen()
{
eraseScreen();
resetAttributes();
moveCursorHome();
}
// Config {{{1
// =============================================================================
enum TITLE = "Seance";
enum MAX_SCORE = 50;
enum MAX_MESSAGE_LENGTH = 6;
enum MIN_MESSAGE_LENGTH = 3;
enum BASE_CHARACTER = '@';
enum PLANCHETTE = '*';
enum SPACE = ' ';
enum FIRST_LETTER_NUMBER = 1;
enum LAST_LETTER_NUMBER = 26;
enum BOARD_INK = BRIGHT + CYAN + FOREGROUND;
enum DEFAULT_INK = WHITE + FOREGROUND;
enum INPUT_INK = BRIGHT + GREEN + FOREGROUND;
enum INSTRUCTIONS_INK = YELLOW + FOREGROUND;
enum MISTAKE_EFFECT_INK = BRIGHT + RED + FOREGROUND;
enum PLANCHETTE_INK = YELLOW + FOREGROUND;
enum TITLE_INK = BRIGHT + RED + FOREGROUND;
enum BOARD_X = 29; // screen column
enum BOARD_Y = 5; // screen line
enum BOARD_HEIGHT = 5; // characters displayed on the left and right borders
enum BOARD_WIDTH = 8; // characters displayed on the top and bottom borders
enum BOARD_PAD = 1; // blank characters separating the board from its left and right borders
enum BOARD_ACTUAL_WIDTH = BOARD_WIDTH + 2 * BOARD_PAD; // screen columns
enum BOARD_BOTTOM_Y = BOARD_HEIGHT + 1; // relative to the board
enum INPUT_X = BOARD_X;
enum INPUT_Y = BOARD_Y + BOARD_BOTTOM_Y + 4;
enum MESSAGES_Y = INPUT_Y;
enum MISTAKE_EFFECT_PAUSE = 3000; // ms
// User input {{{1
// =============================================================================
string acceptString(string prompt)
{
import std.stdio : readln;
import std.stdio : write;
import std.string : strip;
write(prompt);
return strip(readln());
}
void pressEnter(string prompt)
{
acceptString(prompt);
}
// Credits and instructions {{{1
// =============================================================================
void printTitle()
{
import std.stdio : writefln;
setStyle(TITLE_INK);
writefln("%s", TITLE);
setStyle(DEFAULT_INK);
}
void printCredits()
{
import std.stdio : writeln;
printTitle();
writeln("\nOriginal version in BASIC:");
writeln(" Written by Chris Oxlade, 1983.");
writeln(" https://archive.org/details/seance.qb64");
writeln(" https://github.com/chaosotter/basic-games");
writeln("This version in D:");
writeln(" Copyright (c) 2025, Marcos Cruz (programandala.net)");
writeln(" SPDX-License-Identifier: Fair");
}
void printInstructions()
{
import std.stdio : writeln;
printTitle();
setStyle(INSTRUCTIONS_INK);
writeln("\nMessages from the Spirits are coming through, letter by letter. They want you");
writeln("to remember the letters and type them into the computer in the correct order.");
writeln("If you make mistakes, they will be angry -- very angry...");
writeln();
writeln("Watch for stars on your screen -- they show the letters in the Spirits'");
writeln("messages.");
setStyle(DEFAULT_INK);
}
// Game {{{1
// =============================================================================
int randomIntInInclusiveRange(int min, int max)
{
import std.random : uniform;
return uniform(min, max + 1);
}
/// Return the x coordinate to print the given text centered on the board.
int boardCenteredX(string text)
{
return cast(int)(BOARD_X + (BOARD_ACTUAL_WIDTH - text.length) / 2);
}
/// Print the given text on the given row, centered on the board.
void printCentered(string text, int y)
{
import std.stdio : writefln;
setCursorPosition(y, boardCenteredX(text));
writefln("%s", text);
}
/// Print the title on the given row, centered on the board.
void printCenteredTitle(int y)
{
setStyle(TITLE_INK);
printCentered(TITLE, y);
setStyle(DEFAULT_INK);
}
// Print the given letter at the given board coordinates.
void printCharacter(int y, int x, int charCode)
{
import std.conv : to;
import std.stdio : write;
setCursorPosition(y + BOARD_Y, x + BOARD_X);
write(to!(char)(charCode));
}
void printBoard()
{
import std.stdio : writeln;
setStyle(BOARD_INK);
foreach (int i; 1 .. BOARD_WIDTH + 1)
{
printCharacter(0, i + 1, BASE_CHARACTER + i); // top border
printCharacter(BOARD_BOTTOM_Y, i + 1, BASE_CHARACTER + LAST_LETTER_NUMBER - BOARD_HEIGHT - i + 1); // bottom border
}
foreach (int i; 1 .. BOARD_HEIGHT + 1)
{
printCharacter(i , 0, BASE_CHARACTER + LAST_LETTER_NUMBER - i + 1); // left border
printCharacter(i , 3 + BOARD_WIDTH, BASE_CHARACTER + BOARD_WIDTH + i); // right border
}
writeln();
setStyle(DEFAULT_INK);
}
void eraseLineFrom(int line, int column)
{
setCursorPosition(line, column);
eraseLineToEnd();
}
/// Print the given mistake effect, do a pause and erase it.
void printMistakeEffect(string effect)
{
import core.thread.osthread : Thread;
import core.time : msecs;
import std.stdio : writeln;
int x = boardCenteredX(effect);
hideCursor();
setCursorPosition(MESSAGES_Y, x);
setStyle(MISTAKE_EFFECT_INK);
writeln(effect);
setStyle(DEFAULT_INK);
Thread.sleep(MISTAKE_EFFECT_PAUSE.msecs);
eraseLineFrom(MESSAGES_Y, x);
showCursor();
}
/// Return a new message of the given length, after marking its letters on the
/// board.
string message(int length)
{
import core.thread.osthread : Thread;
import core.time : msecs;
import std.conv : to;
import std.stdio : writeln;
const(int) LETTER_PAUSE = 1000; // milliseconds
int y = 0;
int x = 0;
char[] letters;
hideCursor();
foreach (int i; 0 .. length + 1)
{
int letterNumber = randomIntInInclusiveRange(
FIRST_LETTER_NUMBER,
LAST_LETTER_NUMBER);
letters ~= to!(char)(BASE_CHARACTER + letterNumber);
if (letterNumber <= BOARD_WIDTH)
{
// top border
y = 1;
x = letterNumber + 1;
}
else if (letterNumber <= BOARD_WIDTH + BOARD_HEIGHT)
{
// right border
y = letterNumber - BOARD_WIDTH;
x = 2 + BOARD_WIDTH;
}
else if (letterNumber <= BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH)
{
// bottom border
y = BOARD_BOTTOM_Y - 1;
x = 2 + BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH - letterNumber;
}
else
{
// left border
y = 1 + LAST_LETTER_NUMBER - letterNumber;
x = 1;
}
setStyle(PLANCHETTE_INK);
printCharacter(y, x, PLANCHETTE);
writeln();
Thread.sleep(LETTER_PAUSE.msecs);
setStyle(DEFAULT_INK);
printCharacter(y, x, SPACE);
}
showCursor();
return cast(string)(letters);
}
string acceptMessage()
{
import std.uni : toUpper;
setStyle(INPUT_INK);
setCursorPosition(INPUT_Y, INPUT_X);
string result = toUpper(acceptString("? "));
setStyle(DEFAULT_INK);
eraseLineFrom(INPUT_Y, INPUT_X);
return result;
}
void play()
{
int score = 0;
int mistakes = 0;
printCenteredTitle(1);
printBoard();
while (true)
{
int messageLength = randomIntInInclusiveRange(
MIN_MESSAGE_LENGTH,
MAX_MESSAGE_LENGTH
);
string messageReceived = message(messageLength);
string messageUnderstood = acceptMessage();
if (messageReceived != messageUnderstood)
{
mistakes += 1;
final switch (mistakes)
{
case 1:
printMistakeEffect("The table begins to shake!");
break;
case 2:
printMistakeEffect("The light bulb shatters!");
break;
case 3:
printMistakeEffect("Oh, no! A pair of clammy hands grasps your neck!");
return;
}
}
else
{
score += messageLength;
if (score >= MAX_SCORE)
{
printCentered("Whew! The spirits have gone!", MESSAGES_Y);
printCentered("You live to face another day!", MESSAGES_Y + 1);
return;
}
}
}
}
// Main {{{1
// =============================================================================
void main()
{
import std.stdio : writeln;
setStyle(DEFAULT_INK);
clearScreen();
printCredits();
pressEnter("\nPress the Enter key to read the instructions. ");
clearScreen();
printInstructions();
pressEnter("\nPress the Enter key to start. ");
clearScreen();
play();
writeln();
}
In Hare
// Seance
//
// Original version in BASIC:
// By Chris Oxlade, 1983.
// https://archive.org/details/seance.qb64
// https://github.com/chaosotter/basic-games
//
// This version in Hare:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2025-02-17.
//
// Last modified: 20260213T1645+0100.
// Modules {{{1
// =============================================================================
use ascii;
use bufio;
use fmt;
use math::random;
use os;
use strconv;
use strings;
use time;
// Terminal {{{1
// =============================================================================
def BLACK = 0;
def RED = 1;
def GREEN = 2;
def YELLOW = 3;
def BLUE = 4;
def MAGENTA = 5;
def CYAN = 6;
def WHITE = 7;
def DEFAULT = 9;
def STYLE_OFF = 20;
def FOREGROUND = 30;
def BACKGROUND = 40;
def BRIGHT = 60;
def NORMAL_STYLE = 0;
fn move_cursor_home() void = {
fmt::print("\x1B[H")!;
};
fn set_cursor_position(line: int, column: int) void = {
fmt::printf("\x1B[{};{}H", line, column)!;
};
fn hide_cursor() void = {
fmt::print("\x1B[?25l")!;
};
fn show_cursor() void = {
fmt::print("\x1B[?25h")!;
};
fn set_style(style: int) void = {
fmt::printf("\x1B[{}m", style)!;
};
fn reset_attributes() void = {
set_style(NORMAL_STYLE);
};
fn erase_line_to_end() void = {
fmt::print("\x1B[K")!;
};
fn erase_screen() void = {
fmt::print("\x1B[2J")!;
};
fn clear_screen() void = {
erase_screen();
reset_attributes();
move_cursor_home();
};
// Config {{{1
// =============================================================================
def TITLE = "Seance";
def MAX_SCORE = 50;
def MAX_MESSAGE_LENGTH: int = 6;
def MIN_MESSAGE_LENGTH: int = 3;
def BASE_CHARACTER = '@': int;
def PLANCHETTE = '*';
def FIRST_LETTER_NUMBER = 1;
def LAST_LETTER_NUMBER = 26;
def BOARD_INK = FOREGROUND + BRIGHT + CYAN;
def DEFAULT_INK = FOREGROUND + WHITE;
def INPUT_INK = FOREGROUND + BRIGHT + GREEN;
def INSTRUCTIONS_INK = FOREGROUND + YELLOW;
def MISTAKE_EFFECT_INK = FOREGROUND + BRIGHT + RED;
def PLANCHETTE_INK = FOREGROUND + YELLOW;
def TITLE_INK = FOREGROUND + BRIGHT + RED;
def INPUT_X: int = BOARD_X;
def INPUT_Y: int = BOARD_Y + BOARD_BOTTOM_Y + 4;
def MESSAGES_Y = INPUT_Y;
def MISTAKE_EFFECT_PAUSE = 3; // seconds
def BOARD_ACTUAL_WIDTH = BOARD_WIDTH + 2 * BOARD_PAD; // screen columns
def BOARD_BOTTOM_Y = BOARD_HEIGHT + 1; // relative to the board
def BOARD_HEIGHT = 5; // characters displayed on the left and right borders
def BOARD_PAD = 1; // blank characters separating the board from its left and right borders
def BOARD_WIDTH = 8; // characters displayed on the top and bottom borders
def BOARD_X = 29; // screen column
def BOARD_Y = 5; // screen line
// User input {{{1
// =============================================================================
fn print_prompt(prompt: str = "") void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
fn press_enter(prompt: str) void = {
free(accept_string(prompt));
};
// Credits and instructions {{{1
// =============================================================================
fn print_credits() void = {
print_title();
fmt::println("\nOriginal version in BASIC:")!;
fmt::println(" Written by Chris Oxlade, 1983.")!;
fmt::println(" https://archive.org/details/seance.qb64")!;
fmt::println(" https://github.com/chaosotter/basic-games")!;
fmt::println("This version in Hare:")!;
fmt::println(" Copyright (c) 2025, Marcos Cruz (programandala.net)")!;
fmt::println(" SPDX-License-Identifier: Fair")!;
};
fn print_instructions() void = {
print_title();
set_style(INSTRUCTIONS_INK);
defer set_style(DEFAULT_INK);
fmt::println("\nMessages from the Spirits are coming through, letter by letter. They want you")!;
fmt::println("to remember the letters and type them into the computer in the correct order.")!;
fmt::println("If you make mistakes, they will be angry -- very angry...")!;
fmt::println()!;
fmt::println("Watch for stars on your screen -- they show the letters in the Spirits'")!;
fmt::println("messages.")!;
};
// Random numbers {{{1
// =============================================================================
let rand: random::random = 0;
fn randomize() void = {
rand = random::init(time::now(time::clock::MONOTONIC).sec: u64);
};
fn random_int_in_inclusive_range(min: int, max: int) int = {
return random::u32n(&rand, (max - min):u32): int + min;
};
// Game {{{1
// =============================================================================
// Return the x coordinate to print the given text centered on the board.
//
fn board_centered_x(text: str) int = {
return (BOARD_X + (BOARD_ACTUAL_WIDTH - len(text)) / 2): int;
};
// Print the given text on the given row, centered on the board.
//
fn print_board_centered(text: str, y: int) void = {
set_cursor_position(y, board_centered_x(text));
fmt::println(text)!;
};
// Print the title at the current cursor position.
//
fn print_title() void = {
set_style(TITLE_INK);
fmt::println(TITLE)!;
set_style(DEFAULT_INK);
};
// Print the title on the given row, centered on the board.
//
fn print_board_centered_title(y: int) void = {
set_style(TITLE_INK);
print_board_centered(TITLE, y);
set_style(DEFAULT_INK);
};
// Print the given letter at the given board coordinates.
//
fn print_character(y: int, x: int, a: rune) void = {
set_cursor_position(y + BOARD_Y, x + BOARD_X);
fmt::print(a)!;
bufio::flush(os::stdout)!;
};
fn print_board() void = {
set_style(BOARD_INK);
defer set_style(DEFAULT_INK);
for (let i = 1; i <= BOARD_WIDTH; i += 1) {
print_character(0, i + 1, (BASE_CHARACTER + i): rune); // top border
print_character(BOARD_BOTTOM_Y, i + 1, (BASE_CHARACTER + LAST_LETTER_NUMBER - BOARD_HEIGHT - i + 1): rune); // bottom border
};
for (let i = 1; i <= BOARD_HEIGHT; i += 1) {
print_character(i , 0, (BASE_CHARACTER + LAST_LETTER_NUMBER - i + 1): rune); // left border
print_character(i , 3 + BOARD_WIDTH, (BASE_CHARACTER + BOARD_WIDTH + i): rune); // right border
};
fmt::println()!;
};
fn erase_line_from(line: int, column: int) void = {
set_cursor_position(line, column);
erase_line_to_end();
};
fn wait_seconds(seconds: int) void = {
time::sleep(seconds * time::SECOND);
};
// Print the given mistake effect, wait a configured number of seconds and
// finally erase it.
//
fn print_mistake_effect(effect: str) void = {
let x = board_centered_x(effect);
hide_cursor();
set_cursor_position(MESSAGES_Y, x);
set_style(MISTAKE_EFFECT_INK);
fmt::println(effect)!;
set_style(DEFAULT_INK);
wait_seconds(MISTAKE_EFFECT_PAUSE);
erase_line_from(MESSAGES_Y, x);
show_cursor();
};
// Return a new message of the given length, after marking its letters on the
// board.
//
fn message(length: int) str = {
let y: int = 0;
let x: int = 0;
let letters: []rune = [];
hide_cursor();
for (let i = 0; i <= length; i += 1) {
let letter_number: int = random_int_in_inclusive_range(
FIRST_LETTER_NUMBER,
LAST_LETTER_NUMBER
);
append(letters, (BASE_CHARACTER + letter_number): rune)!;
if (letter_number <= BOARD_WIDTH) {
// top border
y = 1;
x = letter_number + 1;
} else if (letter_number <= BOARD_WIDTH + BOARD_HEIGHT) {
// right border
y = letter_number - BOARD_WIDTH;
x = 2 + BOARD_WIDTH;
} else if (letter_number <= BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH) {
// bottom border
y = BOARD_BOTTOM_Y - 1;
x = 2 + BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH - letter_number;
} else {
// left border
y = 1 + LAST_LETTER_NUMBER - letter_number;
x = 1;
};
set_style(PLANCHETTE_INK);
print_character(y, x, PLANCHETTE);
set_style(DEFAULT_INK);
wait_seconds(1);
print_character(y, x, ' ');
};
show_cursor();
return strings::fromrunes(letters)!;
};
fn accept_message() str = {
set_style(INPUT_INK);
defer set_style(DEFAULT_INK);
set_cursor_position(INPUT_Y, INPUT_X);
defer erase_line_from(INPUT_Y, INPUT_X);
return ascii::strupper(accept_string("? "))!;
};
fn play() void = {
let score = 0;
let mistakes = 0;
print_board_centered_title(1);
print_board();
for (true) {
let message_length = random_int_in_inclusive_range(
MIN_MESSAGE_LENGTH,
MAX_MESSAGE_LENGTH
);
let message_received = message(message_length);
defer free(message_received);
let message_understood = accept_message();
defer free(message_understood);
if (message_received != message_understood) {
mistakes += 1;
switch (mistakes) {
case 1 =>
print_mistake_effect("The table begins to shake!");
case 2 =>
print_mistake_effect("The light bulb shatters!");
case 3 =>
print_mistake_effect("Oh, no! A pair of clammy hands grasps your neck!");
return;
case =>
void;
};
} else {
score += message_length;
if (score >= MAX_SCORE) {
print_board_centered("Whew! The spirits have gone!", MESSAGES_Y);
print_board_centered("You live to face another day!", MESSAGES_Y + 1);
return;
};
};
};
};
// Main {{{1
// =============================================================================
export fn main() void = {
randomize();
set_style(DEFAULT_INK);
clear_screen();
print_credits();
press_enter("\nPress the Enter key to read the instructions. ");
clear_screen();
print_instructions();
press_enter("\nPress the Enter key to start. ");
clear_screen();
play();
fmt::println()!;
};
In Julia
#=
Seance
Original version in BASIC:
By Chris Oxlade, 1983.
https://archive.org/details/seance.qb64
https://github.com/chaosotter/basic-games
This version in Julia:
Copyright (c) 2024, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written on 2024-07-05/06.
Last modified: 20240706T0125+0200.
=#
const MAX_SCORE = 50
const MAX_MESSAGE_LENGTH = 6
const MIN_MESSAGE_LENGTH = 3
const BASE_CHARACTER = Int('@')
const PLANCHETTE = '*'
const FIRST_LETTER_NUMBER = 1
const LAST_LETTER_NUMBER = 26
const BOARD_INK = :light_cyan
const DEFAULT_INK = :white
const INPUT_INK = :light_green
const INSTRUCTIONS_INK = :yellow
const MISTAKE_EFFECT_INK = :light_red
const PLANCHETTE_INK = :yellow
const TITLE_INK = :light_red
const MISTAKE_EFFECT_PAUSE = 3 # seconds
const BOARD_WIDTH = 8 # characters displayed on the top and bottom borders
const BOARD_HEIGHT = 5 # characters displayed on the left and right borders
const BOARD_PAD = 1 # blank characters separating the board from its left and right borders
const BOARD_X = 29 # screen column
const BOARD_Y = 5 # screen line
const BOARD_ACTUAL_WIDTH = BOARD_WIDTH + 2 * BOARD_PAD # screen columns
const BOARD_BOTTOM_Y = BOARD_HEIGHT + 1 # relative to the board
const INPUT_X = BOARD_X
const INPUT_Y = BOARD_Y + BOARD_BOTTOM_Y + 4
const MESSAGES_Y = INPUT_Y
# Clear the terminal and move the cursor to the top left position.
function clear_screen()
print("\e[2J\e[H")
end
# Set the cursor position to the given coordinates (the top left position is 1, 1).
function set_cursor_position(line::Int, column::Int)
print("\e[$(line);$(column)H")
end
# Erase from the current cursor position to the end of the current line.
function erase_line_right()
print("\e[K")
end
# Erase the given line to the right of the given column.
function erase_line_right_from(line, column::Int)
set_cursor_position(line, column)
erase_line_right()
end
# Make the cursor invisible.
function hide_cursor()
print("\e[?25l")
end
# Make the cursor visible.
function show_cursor()
print("\e[?25h")
end
# Print the given prompt and wait until the user enters a string.
function input(prompt::String = "")::String
printstyled(prompt, color = INPUT_INK)
return readline()
end
# Print the given prompt and wait until the user presses Enter.
function press_enter(prompt::String)
input(prompt)
end
# Return the x coordinate to print the given text centered on the board.
function board_centered_x(text::String)::Int
return round(BOARD_X + (BOARD_ACTUAL_WIDTH - length(text)) / 2)
end
# Print the given text on the given row, centered on the board, with the given
# or default color.
function print_board_centered(text::String, y::Int, color::Symbol=:default)
set_cursor_position(y, board_centered_x(text))
printstyled(text, color = color)
end
const TITLE = "Seance"
# Print the title at the current cursor position.
function print_title()
printstyled("TITLE\n", color = TITLE_INK)
end
function print_credits()
print_title()
println("\nOriginal version in BASIC:")
println(" Written by Chris Oxlade, 1983.")
println(" https://archive.org/details/seance.qb64")
println(" https://github.com/chaosotter/basic-games")
println("This version in Julia:")
println(" Copyright (c) 2024, Marcos Cruz (programandala.net)")
println(" SPDX-License-Identifier: Fair\n")
end
function print_instructions()
print_title()
printstyled("""
Messages from the Spirits are coming through, letter by letter. They want you
to remember the letters and type them into the computer in the correct order.
If you make mistakes, they will be angry -- very angry...
Watch for stars on your screen -- they show the letters in the Spirits'
messages.\n""", color = INSTRUCTIONS_INK)
end
# Print the given letter at the given board coordinates in the given or default
# color.
function print_character(y, x::Int, a::Char, color::Symbol=:default)
set_cursor_position(y + BOARD_Y, x + BOARD_X)
printstyled(a, color = color)
end
function print_board()
for i in 1 : BOARD_WIDTH
print_character(0, i + 1, Char(BASE_CHARACTER + i), BOARD_INK) # top border
print_character(BOARD_BOTTOM_Y, i + 1, Char(BASE_CHARACTER + LAST_LETTER_NUMBER - BOARD_HEIGHT - i + 1), BOARD_INK) # bottom border
end
for i in 1 : BOARD_HEIGHT
print_character(i , 0, Char(BASE_CHARACTER + LAST_LETTER_NUMBER - i + 1), BOARD_INK) # left border
print_character(i , 3 + BOARD_WIDTH, Char(BASE_CHARACTER + BOARD_WIDTH + i), BOARD_INK) # right border
end
println()
end
# Print the given mistake effect, wait a configured number of seconds and finally erase it.
function print_mistake_effect(effect::String)
x = board_centered_x(effect)
hide_cursor()
set_cursor_position(MESSAGES_Y, x)
printstyled(effect, color = MISTAKE_EFFECT_INK)
sleep(MISTAKE_EFFECT_PAUSE)
erase_line_right_from(MESSAGES_Y, x)
show_cursor()
end
# Return a new message of the given length, after marking its letters on the board.
function message(length::Int)::String
y = 0
x = 0
message = ""
hide_cursor()
for i in 1 : length
letter_number = rand(FIRST_LETTER_NUMBER : LAST_LETTER_NUMBER)
letter = Char(BASE_CHARACTER + letter_number)
message = message * letter # add letter to message
if letter_number <= BOARD_WIDTH
# top border
y = 1
x = letter_number + 1
elseif letter_number <= BOARD_WIDTH + BOARD_HEIGHT
# right border
y = letter_number - BOARD_WIDTH
x = 2 + BOARD_WIDTH
elseif letter_number <= BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH
# bottom border
y = BOARD_BOTTOM_Y - 1
x = 2 + BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH - letter_number
else
# left border
y = 1 + LAST_LETTER_NUMBER - letter_number
x = 1
end
print_character(y, x, PLANCHETTE, PLANCHETTE_INK)
sleep(1)
print_character(y, x, ' ')
end
show_cursor()
return message
end
# Accept a string from the user, erase it from the screen and return it.
function message_understood()::String
set_cursor_position(INPUT_Y, INPUT_X)
user_input = uppercase(input("? "))
erase_line_right_from(INPUT_Y, INPUT_X)
return user_input
end
function play()
score = 0
mistakes = 0
print_board_centered(TITLE, 1, TITLE_INK)
print_board()
while true
message_length = rand(MIN_MESSAGE_LENGTH : MAX_MESSAGE_LENGTH)
if message(message_length) != message_understood()
mistakes += 1
if mistakes == 1
print_mistake_effect("The table begins to shake!")
elseif mistakes == 2
print_mistake_effect("The light bulb shatters!")
elseif mistakes == 3
print_mistake_effect("Oh, no! A pair of clammy hands grasps your neck!")
return
end
else
score += message_length
if score >= MAX_SCORE
print_board_centered("Whew! The spirits have gone!", MESSAGES_Y)
print_board_centered("You live to face another day!", MESSAGES_Y + 1)
return
end
end
end
end
function main()
clear_screen()
print_credits()
press_enter("\nPress the Enter key to read the instructions. ")
clear_screen()
print_instructions()
press_enter("\nPress the Enter key to start. ")
clear_screen()
play()
println("\n")
end
main()
In Kotlin
/*
Seance
Original version in BASIC:
By Chris Oxlade, 1983.
https://archive.org/details/seance.qb64
https://github.com/chaosotter/basic-games
This version in Kotlin:
Copyright (c) 2023, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written on 2023-11-26.
Last modified: 20250519T0008+0200.
*/
import kotlin.time.*
// Terminal {{{1
// =============================================================
const val BLACK = 0
const val RED = 1
const val GREEN = 2
const val YELLOW = 3
const val BLUE = 4
const val MAGENTA = 5
const val CYAN = 6
const val WHITE = 7
const val DEFAULT = 9
const val FOREGROUND = +30
const val BRIGHT = +60
// Move the cursor to the top left position of the terminal.
fun moveCursorHome() {
print("\u001B[H")
}
// Clear the terminal and move the cursor to the top left position.
fun clearScreen() {
print("\u001B[2J")
moveCursorHome()
}
// Set the color.
fun setColor(color: Int) {
print("\u001B[${color}m")
}
// Set the cursor position to the given coordinates (the top left position is 1, 1)
fun setCursorPosition(line: Int, column: Int) {
print("\u001B[${line};${column}H")
}
// Make the cursor invisible.
fun hideCursor() {
print("\u001B[?25l")
}
// Make the cursor visible.
fun showCursor() {
print("\u001B[?25h")
}
// Erase from the current cursor position to the end of the current line.
fun eraseLineRight() {
print("\u001B[K")
}
// Erase the given line to the right of the given column.
fun eraseLineRightFrom(line: Int, column: Int) {
setCursorPosition(line, column)
eraseLineRight()
}
// Global variables and constants {{{1
// =============================================================
const val MAX_SCORE = 50
const val MAX_MESSAGE_LENGTH = 6
const val MIN_MESSAGE_LENGTH = 3
const val BASE_CHARACTER = '@'.code
const val PLANCHETTE = '*'
const val FIRST_LETTER_NUMBER = 1
const val LAST_LETTER_NUMBER = 26
const val BOARD_INK = BRIGHT + CYAN + FOREGROUND
const val DEFAULT_INK = WHITE + FOREGROUND
const val INPUT_INK = BRIGHT + GREEN + FOREGROUND
const val INSTRUCTIONS_INK = YELLOW + FOREGROUND
const val MISTAKE_EFFECT_INK = BRIGHT + RED + FOREGROUND
const val PLANCHETTE_INK = YELLOW + FOREGROUND
const val TITLE_INK = BRIGHT + RED + FOREGROUND
const val MISTAKE_EFFECT_PAUSE: Long = 3 // seconds
const val BOARD_HEIGHT = 5 // characters displayed on the left and right borders
const val BOARD_BOTTOM_Y = BOARD_HEIGHT + 1 // relative to the board
const val BOARD_PAD = 1 // blank characters separating the board from its left and right borders
const val BOARD_WIDTH = 8 // characters displayed on the top and bottom borders
const val BOARD_ACTUAL_WIDTH = BOARD_WIDTH + 2 * BOARD_PAD // screen columns
const val BOARD_X = 29 // screen column
const val BOARD_Y = 5 // screen line
const val INPUT_X = BOARD_X
const val INPUT_Y = BOARD_Y + BOARD_BOTTOM_Y + 4
const val MESSAGES_Y = INPUT_Y
const val TITLE = "Seance"
// User input {{{1
// =============================================================
// Print the given prompt and wait until the user enters a string.
fun input(prompt: String = ""): String {
setColor(INPUT_INK)
print(prompt)
val result: String = readln() ?: ""
setColor(DEFAULT_INK)
return result
}
// Print the given prompt and wait until the user presses Enter.
fun pressEnter(prompt: String) {
input(prompt)
}
// Accept a string from the user, erase it from the screen and return it.
fun messageUnderstood(): String {
setCursorPosition(INPUT_Y, INPUT_X)
val result = input("? ").uppercase()
eraseLineRightFrom(INPUT_Y, INPUT_X)
return result
}
// Title, instructions and credits {{{1
// =============================================================
// Print the title at the current cursor position.
fun printTitle() {
setColor(TITLE_INK)
println(TITLE)
setColor(DEFAULT_INK)
}
// Print the title on the given row, centered on the board.
fun printBoardCenteredTitle(y: Int) {
setColor(TITLE_INK)
printBoardCentered(TITLE, y)
setColor(DEFAULT_INK)
}
fun printCredits() {
printTitle()
println("\nOriginal version in BASIC:")
println(" Written by Chris Oxlade, 1983.")
println(" https://archive.org/details/seance.qb64")
println(" https://github.com/chaosotter/basic-games")
println("This version in Kotlin:")
println(" Copyright (c) 2023, Marcos Cruz (programandala.net)")
println(" SPDX-License-Identifier: Fair\n")
}
fun printInstructions() {
printTitle()
setColor(INSTRUCTIONS_INK)
println("\nMessages from the Spirits are coming through, letter by letter. They want you")
println("to remember the letters and type them into the computer in the correct order.")
println("If you make mistakes, they will be angry -- very angry...")
println()
println("Watch for stars on your screen -- they show the letters in the Spirits'")
println("messages.")
setColor(DEFAULT_INK)
}
// Time {{{1
// =============================================================
// Wait the given number of seconds.
//
// Note `Thread.sleep(1000 * seconds)` (provided by `import
// kotlin.concurrent.thread`) would be a simpler alternative, but it's not
// supported by kotlinc-native.
//
fun waitSeconds(seconds: Long) {
val duration: Duration = seconds.toDuration(DurationUnit.SECONDS)
val timeSource = TimeSource.Monotonic
val startTime = timeSource.markNow()
do {
var currentTime = timeSource.markNow()
} while ((currentTime - startTime) < duration)
}
// Board {{{1
// =============================================================
// Return the x coordinate to print the given text centered on the board.
fun boardCenteredX(text: String): Int {
return BOARD_X + (BOARD_ACTUAL_WIDTH - text.length) / 2
}
// Print the given text on the given row, centered on the board.
fun printBoardCentered(text: String, y: Int) {
setCursorPosition(y, boardCenteredX(text))
println(text)
}
// Print the given letter at the given board coordinates.
fun printCharacter(y: Int, x: Int, a: Char) {
setCursorPosition(y + BOARD_Y, x + BOARD_X)
print(a)
}
// Print the board.
fun printBoard() {
setColor(BOARD_INK)
for (i in 1..BOARD_WIDTH) {
printCharacter(0, i + 1, (BASE_CHARACTER + i).toChar()) // top border
printCharacter(BOARD_BOTTOM_Y, i + 1, (BASE_CHARACTER + LAST_LETTER_NUMBER - BOARD_HEIGHT - i + 1).toChar()) // bottom border
}
for (i in 1..BOARD_HEIGHT) {
printCharacter(i , 0, (BASE_CHARACTER + LAST_LETTER_NUMBER - i + 1).toChar()) // left border
printCharacter(i , 3 + BOARD_WIDTH, (BASE_CHARACTER + BOARD_WIDTH + i).toChar()) // right border
}
println()
setColor(DEFAULT_INK)
}
// Print the given mistake effect, wait a configured number of seconds and finally erase it.
fun printMistakeEffect(effect: String) {
var x = boardCenteredX(effect)
hideCursor()
setCursorPosition(MESSAGES_Y, x)
setColor(MISTAKE_EFFECT_INK)
println(effect)
setColor(DEFAULT_INK)
waitSeconds(MISTAKE_EFFECT_PAUSE)
eraseLineRightFrom(MESSAGES_Y, x)
showCursor()
}
// Return a new message of the given length, after marking its letters on the board.
fun message(length: Int): String {
var y: Int
var x: Int
var message = ""
hideCursor()
for (i in 1..length) {
var letterNumber = (FIRST_LETTER_NUMBER..LAST_LETTER_NUMBER).random()
var letter: Char = (BASE_CHARACTER + letterNumber).toChar()
message = "$message$letter"
when {
letterNumber <= BOARD_WIDTH -> {
// top border
y = 1
x = letterNumber + 1
}
letterNumber <= BOARD_WIDTH + BOARD_HEIGHT -> {
// right border
y = letterNumber - BOARD_WIDTH
x = 2 + BOARD_WIDTH
}
letterNumber <= BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH -> {
// bottom border
y = BOARD_BOTTOM_Y - 1
x = 2 + BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH - letterNumber
}
else -> {
// left border
y = 1 + LAST_LETTER_NUMBER - letterNumber
x = 1
}
}
setColor(PLANCHETTE_INK)
printCharacter(y, x, PLANCHETTE)
setColor(DEFAULT_INK)
waitSeconds(1)
printCharacter(y, x, ' ')
}
showCursor()
return message
}
// Game loop {{{1
// =============================================================
fun play() {
var score = 0
var mistakes = 0
printBoardCenteredTitle(1)
printBoard()
while (true) {
var messageLength = (MIN_MESSAGE_LENGTH..MAX_MESSAGE_LENGTH).random()
if (message(messageLength) != messageUnderstood()) {
mistakes += 1
when (mistakes) {
1 ->
printMistakeEffect("The table begins to shake!")
2 ->
printMistakeEffect("The light bulb shatters!")
3 -> {
printMistakeEffect("Oh, no! A pair of clammy hands grasps your neck!")
return
}
}
} else {
score += messageLength
if (score >= MAX_SCORE) {
printBoardCentered("Whew! The spirits have gone!", MESSAGES_Y)
printBoardCentered("You live to face another day!", MESSAGES_Y + 1)
return
}
}
}
}
// Main {{{1
// =============================================================
fun main() {
setColor(DEFAULT_INK)
clearScreen()
printCredits()
pressEnter("\nPress the Enter key to read the instructions. ")
clearScreen()
printInstructions()
pressEnter("\nPress the Enter key to start. ")
clearScreen()
play()
println("\n")
}
In Nim
#[
Seance
Original version in BASIC:
By Chris Oxlade, 1983.
https://archive.org/details/seance.qb64
https://github.com/chaosotter/basic-games
This version in Nim:
Copyright (c) 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written on 2025-01-21.
Last modified: 20250121T1531+0100.
]#
import os
import std/math
import std/random
import std/strutils
import std/terminal
import std/unicode
proc cursorHome() =
setCursorPos(0, 0)
proc clearScreen() =
eraseScreen()
cursorHome()
const maxScore = 50
const maxMessageLength = 6
const minMessageLength = 3
const baseCharacter = int('@')
const planchette = '*'
const firstLetterNumber = 1
const lastLetterNumber = 26
type Ink = tuple [
foreground: ForegroundColor,
bright: bool,
]
const boardInk: Ink = (fgCyan, true)
const defaultInk: Ink = (fgWhite, false)
const inputInk: Ink = (fgGreen, true)
const instructionsInk: Ink = (fgYellow, false)
const mistakeEffectInk: Ink = (fgRed, false)
const planchetteInk: Ink = (fgYellow, false)
const titleInk: Ink = (fgRed, false)
const mistakeEffectPause = 3 # seconds
const boardX = 29 # screen column
const boardY = 5 # screen line
const boardWidth = 8 # characters displayed on the top and bottom borders
const boardHeight = 5 # characters displayed on the left and right borders
const boardPad = 1 # blank characters separating the board from its left and right borders
const boardBottomY = boardHeight + 1 # relative to the board
const boardActualWidth = boardWidth + 2 * boardPad # screen columns
const inputX = boardX
const inputY = boardY + boardBottomY + 4
const messagesY = inputY
# Print the given prompt and wait until the user enters a string.
#
proc input(prompt: string = ""): string =
write(stdout, ansiForegroundColorCode(inputInk.foreground, inputInk.bright))
defer: write(stdout, ansiForegroundColorCode(defaultInk.foreground, defaultInk.bright))
write(stdout, prompt)
result = readLine(stdin)
# Print the given prompt and wait until the user presses Enter.
#
proc pressEnter(prompt: string) =
discard input(prompt)
# Return the x coordinate to print the given text centered on the board.
#
proc boardCenteredX(text: string): int =
result = boardX + int((boardActualWidth - len(text)) / 2)
# Print the given text on the given row, centered on the board.
#
proc printBoardCentered(text: string, y: int) =
setCursorPos(boardCenteredX(text), y)
writeLine(stdout, text)
const title = "Seance"
# Print the title at the current cursor position.
#
proc printTitle() =
write(stdout, ansiForegroundColorCode(titleInk.foreground, titleInk.bright))
writeLine(stdout, title)
write(stdout, ansiForegroundColorCode(defaultInk.foreground, defaultInk.bright))
# Print the title on the given row, centered on the board.
#
proc printBoardCenteredTitle(y: int) =
write(stdout, ansiForegroundColorCode(titleInk.foreground, titleInk.bright))
printBoardCentered(title, y)
write(stdout, ansiForegroundColorCode(defaultInk.foreground, defaultInk.bright))
proc printCredits() =
printTitle()
writeLine(stdout, "\nOriginal version in BASIC:")
writeLine(stdout, " Written by Chris Oxlade, 1983.")
writeLine(stdout, " https://archive.org/details/seance.qb64")
writeLine(stdout, " https://github.com/chaosotter/basic-games")
writeLine(stdout, "This version in Nim:")
writeLine(stdout, " Copyright (c) 2025, Marcos Cruz (programandala.net)")
writeLine(stdout, " SPDX-License-Identifier: Fair")
proc printInstructions() =
printTitle()
write(stdout, ansiForegroundColorCode(instructionsInk.foreground, instructionsInk.bright))
defer: write(stdout, ansiForegroundColorCode(defaultInk.foreground, defaultInk.bright))
writeLine(stdout, "\nMessages from the Spirits are coming through, letter by letter. They want you")
writeLine(stdout, "to remember the letters and type them into the computer in the correct order.")
writeLine(stdout, "If you make mistakes, they will be angry -- very angry...")
writeLine(stdout, "")
writeLine(stdout, "Watch for stars on your screen -- they show the letters in the Spirits'")
writeLine(stdout, "messages.")
# Print the given letter at the given board coordinates.
#
proc printCharacter(y, x: int, a: char) =
setCursorPos(x + boardX, y + boardY)
writeLine(stdout, a)
proc printBoard() =
write(stdout, ansiForegroundColorCode(boardInk.foreground, boardInk.bright))
defer: write(stdout, ansiForegroundColorCode(defaultInk.foreground, defaultInk.bright))
for i in 1 .. boardWidth:
printCharacter(0, i + 1, char(baseCharacter + i)) # top border
printCharacter(boardBottomY, i + 1, char(baseCharacter + lastLetterNumber - boardHeight - i + 1)) # bottom border
for i in 1 .. boardHeight:
printCharacter(i , 0, char(baseCharacter + lastLetterNumber - i + 1)) # left border
printCharacter(i , 3 + boardWidth, char(baseCharacter + boardWidth + i)) # right border
writeLine(stdout, "")
proc eraseLineRight() =
write(stdout, "\e[K")
# Erase the given line to the right of the given column.
#
proc eraseLineRightFrom(line, column: int) =
setCursorPos(column, line)
eraseLineRight()
# Wait the given number of seconds.
#
proc waitSeconds(seconds: int) =
sleep(seconds * 1000)
# Print the given mistake effect, wait a configured number of seconds and
# finally erase it.
#
proc printMistakeEffect(effect: string) =
var x = boardCenteredX(effect)
hideCursor()
setCursorPos(x, messagesY)
write(stdout, ansiForegroundColorCode(mistakeEffectInk.foreground, mistakeEffectInk.bright))
writeLine(stdout, effect)
defer: write(stdout, ansiForegroundColorCode(defaultInk.foreground, defaultInk.bright))
waitSeconds(mistakeEffectPause)
eraseLineRightFrom(messagesY, x)
showCursor()
# Return a new message of the given length, after marking its letters on the
# board.
#
proc message(length: int): string =
var y: int
var x: int
var message = ""
hideCursor()
for _ in 1 .. length:
var letterNumber = rand(firstLetterNumber .. lastLetterNumber)
var letter = char(baseCharacter + letterNumber)
message = message & letter
if letterNumber <= boardWidth:
# top border
y = 1
x = letterNumber + 1
elif letterNumber <= boardWidth + boardHeight:
# right border
y = letterNumber - boardWidth
x = 2 + boardWidth
elif letterNumber <= boardWidth + boardHeight + boardWidth:
# bottom border
y = boardBottomY - 1
x = 2 + boardWidth + boardHeight + boardWidth - letterNumber
else:
# left border
y = 1 + lastLetterNumber - letterNumber
x = 1
write(stdout, ansiForegroundColorCode(planchetteInk.foreground, planchetteInk.bright))
printCharacter(y, x, planchette)
write(stdout, ansiForegroundColorCode(defaultInk.foreground, defaultInk.bright))
waitSeconds(1)
printCharacter(y, x, ' ')
showCursor()
result = message
# Accept a string from the user, erase it from the screen and return it.
#
proc messageUnderstood(): string =
setCursorPos(inputX, inputY)
defer: eraseLineRightFrom(inputY, inputX)
result = toUpper(input("? "))
proc play() =
var score = 0
var mistakes = 0
printBoardCenteredTitle(1)
printBoard()
while true:
var messageLength = rand(minMessageLength .. maxMessageLength)
if message(messageLength) != messageUnderstood():
mistakes += 1
case mistakes:
of 1:
printMistakeEffect("The table begins to shake!")
of 2:
printMistakeEffect("The light bulb shatters!")
of 3:
printMistakeEffect("Oh, no! A pair of clammy hands grasps your neck!")
return
else:
discard
else:
score += messageLength
if score >= maxScore:
printBoardCentered("Whew! The spirits have gone!", messagesY)
printBoardCentered("You live to face another day!", messagesY + 1)
return
randomize()
write(stdout, ansiForegroundColorCode(defaultInk.foreground, defaultInk.bright))
clearScreen()
printCredits()
pressEnter("\nPress the Enter key to read the instructions. ")
clearScreen()
printInstructions()
pressEnter("\nPress the Enter key to start. ")
clearScreen()
play()
writeLine(stdout, "\n")
In Odin
/*
Seance
Original version in BASIC:
By Chris Oxlade, 1983.
https://archive.org/details/seance.qb64
https://github.com/chaosotter/basic-games
This version in Odin:
Copyright (c) 2023, 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written in 2023-11, 2023-12, 2025-02.
Last modified: 20250227T0037+0100.
*/
package seance
import "../lib/anodino/src/basic"
import "../lib/anodino/src/read"
import "../lib/anodino/src/term"
import "core:fmt"
import "core:math/rand"
import "core:strings"
import "core:time"
MAX_SCORE :: 50
MAX_MESSAGE_LENGTH :: 6
MIN_MESSAGE_LENGTH :: 3
BASE_CHARACTER :: int('@')
PLANCHETTE :: '*'
FIRST_LETTER_NUMBER :: 1
LAST_LETTER_NUMBER :: 26
BOARD_INK :: basic.BRIGHT_CYAN
DEFAULT_INK :: basic.WHITE
INPUT_INK :: basic.BRIGHT_GREEN
INSTRUCTIONS_INK :: basic.YELLOW
MISTAKE_EFFECT_INK :: basic.BRIGHT_RED
PLANCHETTE_INK :: basic.YELLOW
TITLE_INK :: basic.BRIGHT_RED
INPUT_X :: BOARD_X
INPUT_Y :: BOARD_Y + BOARD_BOTTOM_Y + 4
MESSAGES_Y :: INPUT_Y
MISTAKE_EFFECT_PAUSE :: 3 // seconds
BOARD_ACTUAL_WIDTH :: BOARD_WIDTH + 2 * BOARD_PAD // screen columns
BOARD_BOTTOM_Y :: BOARD_HEIGHT + 1 // relative to the board
BOARD_HEIGHT :: 5 // characters displayed on the left and right borders
BOARD_PAD :: 1 // blank characters separating the board from its left and right borders
BOARD_WIDTH :: 8 // characters displayed on the top and bottom borders
BOARD_X :: 29 // screen column
BOARD_Y :: 5 // screen line
// Print the given prompt and wait until the user enters a string.
//
input :: proc(prompt : string = "") -> (string, bool) {
basic.color(INPUT_INK)
defer basic.color(DEFAULT_INK)
fmt.print(prompt)
return read.a_string()
}
// Print the given prompt and wait until the user presses Enter.
//
press_enter :: proc(prompt : string) {
s, _ := input(prompt)
delete(s)
}
// Return the x coordinate to print the given text centered on the board.
//
board_centered_x :: proc(text : string) -> int {
return BOARD_X + (BOARD_ACTUAL_WIDTH - len(text)) / 2
}
// Print the given text on the given row, centered on the board.
//
print_board_centered :: proc(text : string, y : int) {
term.set_cursor_position(y, board_centered_x(text))
fmt.println(text)
}
TITLE :: "Seance"
// Print the title at the current cursor position.
//
print_title :: proc() {
basic.color(TITLE_INK)
fmt.println(TITLE)
basic.color(DEFAULT_INK)
}
// Print the title on the given row, centered on the board.
//
print_board_centered_title :: proc(y : int) {
basic.color(TITLE_INK)
print_board_centered(TITLE, y)
basic.color(DEFAULT_INK)
}
print_credits :: proc() {
print_title()
fmt.println("\nOriginal version in BASIC:")
fmt.println(" Written by Chris Oxlade, 1983.")
fmt.println(" https://archive.org/details/seance.qb64")
fmt.println(" https://github.com/chaosotter/basic-games")
fmt.println("This version in Odin:")
fmt.println(" Copyright (c) 2023, 2025, Marcos Cruz (programandala.net)")
fmt.println(" SPDX-License-Identifier: Fair\n")
}
print_instructions :: proc() {
print_title()
basic.color(INSTRUCTIONS_INK)
defer basic.color(DEFAULT_INK)
fmt.println("\nMessages from the Spirits are coming through, letter by letter. They want you")
fmt.println("to remember the letters and type them into the computer in the correct order.")
fmt.println("If you make mistakes, they will be angry -- very angry...")
fmt.println()
fmt.println("Watch for stars on your screen -- they show the letters in the Spirits'")
fmt.println("messages.")
}
// Print the given letter at the given board coordinates.
//
print_character :: proc(y, x : int, a : rune) {
term.set_cursor_position(y + BOARD_Y, x + BOARD_X)
fmt.print(a)
}
print_board :: proc() {
basic.color(BOARD_INK)
defer basic.color(DEFAULT_INK)
for i in 1 ..= BOARD_WIDTH {
print_character(0, i + 1, rune(BASE_CHARACTER + i)) // top border
print_character(BOARD_BOTTOM_Y, i + 1, rune(BASE_CHARACTER + LAST_LETTER_NUMBER - BOARD_HEIGHT - i + 1)) // bottom border
}
for i in 1 ..= BOARD_HEIGHT {
print_character(i , 0, rune(BASE_CHARACTER + LAST_LETTER_NUMBER - i + 1)) // left border
print_character(i , 3 + BOARD_WIDTH, rune(BASE_CHARACTER + BOARD_WIDTH + i)) // right border
}
fmt.println()
}
// Return a random integer in the given inclusive range.
//
random_in_inclusive_range :: proc(min, max : int) -> int {
return rand.int_max(max - min + 1) + min
}
// Erase the given line to the right of the given column.
//
erase_line_right_from :: proc(line, column : int) {
term.set_cursor_position(line, column)
term.erase_line_right()
}
// Wait the given number of seconds.
//
wait_seconds :: proc(seconds : int) {
time.sleep(time.Duration(seconds) * time.Second)
}
// Print the given mistake effect, wait a configured number of seconds and
// finally erase it.
//
print_mistake_effect :: proc(effect : string) {
x := board_centered_x(effect)
term.hide_cursor()
term.set_cursor_position(MESSAGES_Y, x)
basic.color(MISTAKE_EFFECT_INK)
fmt.println(effect)
basic.color(DEFAULT_INK)
wait_seconds(MISTAKE_EFFECT_PAUSE)
erase_line_right_from(MESSAGES_Y, x)
term.show_cursor()
}
// Return a new message of the given length, after marking its letters on the
// board.
//
message :: proc(length : int) -> string {
y, x : int
message := ""
term.hide_cursor()
for _ in 1 ..= length {
letter_number := random_in_inclusive_range(FIRST_LETTER_NUMBER, LAST_LETTER_NUMBER)
letter := rune(BASE_CHARACTER + letter_number)
message = fmt.tprintf("%v%v", message, letter) // add letter to message
switch {
case letter_number <= BOARD_WIDTH :
// top border
y = 1
x = letter_number + 1
case letter_number <= BOARD_WIDTH + BOARD_HEIGHT :
// right border
y = letter_number - BOARD_WIDTH
x = 2 + BOARD_WIDTH
case letter_number <= BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH :
// bottom border
y = BOARD_BOTTOM_Y - 1
x = 2 + BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH - letter_number
case :
// left border
y = 1 + LAST_LETTER_NUMBER - letter_number
x = 1
}
basic.color(PLANCHETTE_INK)
print_character(y, x, PLANCHETTE)
basic.color(DEFAULT_INK)
wait_seconds(1)
print_character(y, x, ' ')
}
term.show_cursor()
return message
}
// Accept a string from the user, erase it from the screen and return it.
//
message_understood :: proc() -> string {
term.set_cursor_position(INPUT_Y, INPUT_X)
defer erase_line_right_from(INPUT_Y, INPUT_X)
s, _ := input("? ")
defer delete(s)
return strings.to_upper(s)
}
play :: proc() {
score := 0
mistakes := 0
print_board_centered_title(1)
print_board()
for {
message_length := random_in_inclusive_range(MIN_MESSAGE_LENGTH, MAX_MESSAGE_LENGTH)
msg := message(message_length)
defer delete(msg)
msg_understood := message_understood()
defer delete(msg_understood)
if msg != msg_understood {
mistakes += 1
switch mistakes {
case 1 :
print_mistake_effect("The table begins to shake!")
case 2 :
print_mistake_effect("The light bulb shatters!")
case 3 :
print_mistake_effect("Oh, no! A pair of clammy hands grasps your neck!")
return
}
} else {
score += message_length
if score >= MAX_SCORE {
print_board_centered("Whew! The spirits have gone!", MESSAGES_Y)
print_board_centered("You live to face another day!", MESSAGES_Y + 1)
return
}
}
}
}
main :: proc() {
basic.randomize()
basic.color(DEFAULT_INK)
term.clear_screen()
print_credits()
press_enter("\nPress the Enter key to read the instructions. ")
term.clear_screen()
print_instructions()
press_enter("\nPress the Enter key to start. ")
term.clear_screen()
play()
fmt.println("\n")
}
In Pike
#!/usr/bin/env pike
// Seance
// Original version in BASIC:
// By Chris Oxlade, 1983.
// https://archive.org/details/seance.qb64
// https://github.com/chaosotter/basic-games
// This version in Pike:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2025-03-12.
//
// Last modified: 20250731T1954+0200.
// Terminal {{{1
// =============================================================================
constant BLACK = 0;
constant RED = 1;
constant GREEN = 2;
constant YELLOW = 3;
constant BLUE = 4;
constant MAGENTA = 5;
constant CYAN = 6;
constant WHITE = 7;
constant DEFAULT = 9;
constant STYLE_OFF = 20;
constant FOREGROUND = 30;
constant BACKGROUND = 40;
constant BRIGHT = 60;
constant NORMAL_STYLE = 0;
void move_cursor_home() {
write("\x1B[H");
}
void set_cursor_position(int y, int x) {
write("\x1B[%d;%dH", y, x);
}
void hide_cursor() {
write("\x1B[?25l");
}
void show_cursor() {
write("\x1B[?25h");
}
void set_style(int style) {
write("\x1B[%dm", style);
}
void reset_attributes() {
set_style(NORMAL_STYLE);
}
void erase_line_to_end() {
write("\x1B[K");
}
void erase_screen() {
write("\x1B[2J");
}
void clear_screen() {
erase_screen();
reset_attributes();
move_cursor_home();
}
// Config {{{1
// =============================================================================
constant TITLE = "Seance";
constant MAX_SCORE = 50;
constant MAX_MESSAGE_LENGTH = 6;
constant MIN_MESSAGE_LENGTH = 3;
constant BASE_CHARACTER = '@';
constant PLANCHETTE = '*';
constant SPACE = ' ';
constant FIRST_LETTER_NUMBER = 1;
constant LAST_LETTER_NUMBER = 26;
constant BOARD_INK = BRIGHT + CYAN + FOREGROUND;
constant DEFAULT_INK = WHITE + FOREGROUND;
constant INPUT_INK = BRIGHT + GREEN + FOREGROUND;
constant INSTRUCTIONS_INK = YELLOW + FOREGROUND;
constant MISTAKE_EFFECT_INK = BRIGHT + RED + FOREGROUND;
constant PLANCHETTE_INK = YELLOW + FOREGROUND;
constant TITLE_INK = BRIGHT + RED + FOREGROUND;
constant BOARD_X = 29; // screen column
constant BOARD_Y = 5; // screen line
constant BOARD_HEIGHT = 5; // characters displayed on the left and right borders
constant BOARD_WIDTH = 8; // characters displayed on the top and bottom borders
constant BOARD_PAD = 1; // blank characters separating the board from its left and right borders
constant BOARD_ACTUAL_WIDTH = BOARD_WIDTH + 2 * BOARD_PAD; // screen columns
constant BOARD_BOTTOM_Y = BOARD_HEIGHT + 1; // relative to the board
constant INPUT_X = BOARD_X;
constant INPUT_Y = BOARD_Y + BOARD_BOTTOM_Y + 4;
constant MESSAGES_Y = INPUT_Y;
constant MISTAKE_EFFECT_PAUSE = 3; // seconds
// User input {{{1
// =============================================================================
string accept_string(string prompt) {
write(prompt);
return Stdio.stdin->gets();
}
void press_enter(string prompt) {
accept_string(prompt);
}
// Credits and instructions {{{1
// =============================================================================
void print_title() {
set_style(TITLE_INK);
write("%s\n", TITLE);
set_style(DEFAULT_INK);
}
void print_credits() {
print_title();
write("\nOriginal version in BASIC:\n");
write(" Written by Chris Oxlade, 1983.\n");
write(" https://archive.org/details/seance.qb64\n");
write(" https://github.com/chaosotter/basic-games\n");
write("This version in Pike:\n");
write(" Copyright (c) 2025, Marcos Cruz (programandala.net)\n");
write(" SPDX-License-Identifier: Fair\n");
}
void print_instructions() {
print_title();
set_style(INSTRUCTIONS_INK);
write("\nMessages from the Spirits are coming through, letter by letter. They want you\n");
write("to remember the letters and type them into the computer in the correct order.\n");
write("If you make mistakes, they will be angry -- very angry...\n");
write("\n");
write("Watch for stars on your screen -- they show the letters in the Spirits'\n");
write("messages.\n");
set_style(DEFAULT_INK);
}
// Game {{{1
// =============================================================================
int random_int_in_inclusive_range(int min, int max) {
return random((max - min)) + min;
}
// Return the x coordinate to print the given text centered on the board.
//
int board_centered_x(string text) {
return (BOARD_X + (BOARD_ACTUAL_WIDTH - sizeof(text)) / 2);
}
// Print the given text on the given row, centered on the board.
//
void print_centered(string text, int y) {
set_cursor_position(y, board_centered_x(text));
write("%s\n", text);
}
// Print the title on the given row, centered on the board.
//
void print_centered_title(int y) {
set_style(TITLE_INK);
print_centered(TITLE, y);
set_style(DEFAULT_INK);
}
// Print the given letter at the given board coordinates.
//
void print_character(int y, int x, int char) {
set_cursor_position(y + BOARD_Y, x + BOARD_X);
write(sprintf("%c", char));
}
void print_board() {
set_style(BOARD_INK);
for (int i = 1; i <= BOARD_WIDTH; i += 1) {
print_character(0, i + 1, BASE_CHARACTER + i); // top border
print_character(BOARD_BOTTOM_Y, i + 1, BASE_CHARACTER + LAST_LETTER_NUMBER - BOARD_HEIGHT - i + 1); // bottom border
}
for (int i = 1; i <= BOARD_HEIGHT; i += 1) {
print_character(i , 0, BASE_CHARACTER + LAST_LETTER_NUMBER - i + 1); // left border
print_character(i , 3 + BOARD_WIDTH, BASE_CHARACTER + BOARD_WIDTH + i); // right border
}
write("\n");
set_style(DEFAULT_INK);
}
void erase_line_from(int line, int column) {
set_cursor_position(line, column);
erase_line_to_end();
}
// Print the given mistake effect, wait a configured number of seconds and
// finally erase it.
//
void print_mistake_effect(string effect) {
int x = board_centered_x(effect);
hide_cursor();
set_cursor_position(MESSAGES_Y, x);
set_style(MISTAKE_EFFECT_INK);
write("%s\n", effect);
set_style(DEFAULT_INK);
sleep(MISTAKE_EFFECT_PAUSE);
erase_line_from(MESSAGES_Y, x);
show_cursor();
}
// Return a new message of the given length, after marking its letters on the
// board.
//
string message(int length) {
int y = 0;
int x = 0;
string letters = "";
hide_cursor();
for (int i = 0; i <= length; i += 1) {
int letter_number = random_int_in_inclusive_range(
FIRST_LETTER_NUMBER,
LAST_LETTER_NUMBER
);
letters += sprintf("%c", BASE_CHARACTER + letter_number);
if (letter_number <= BOARD_WIDTH) {
// top border
y = 1;
x = letter_number + 1;
} else if (letter_number <= BOARD_WIDTH + BOARD_HEIGHT) {
// right border
y = letter_number - BOARD_WIDTH;
x = 2 + BOARD_WIDTH;
} else if (letter_number <= BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH) {
// bottom border
y = BOARD_BOTTOM_Y - 1;
x = 2 + BOARD_WIDTH + BOARD_HEIGHT + BOARD_WIDTH - letter_number;
} else {
// left border
y = 1 + LAST_LETTER_NUMBER - letter_number;
x = 1;
}
set_style(PLANCHETTE_INK);
print_character(y, x, PLANCHETTE);
set_style(DEFAULT_INK);
sleep(1);
print_character(y, x, SPACE);
}
show_cursor();
return letters;
}
string accept_message() {
set_style(INPUT_INK);
set_cursor_position(INPUT_Y, INPUT_X);
string result = upper_case(accept_string("? "));
set_style(DEFAULT_INK);
erase_line_from(INPUT_Y, INPUT_X);
return result;
}
void play() {
int score = 0;
int mistakes = 0;
print_centered_title(1);
print_board();
while (true) {
int message_length = random_int_in_inclusive_range(
MIN_MESSAGE_LENGTH,
MAX_MESSAGE_LENGTH
);
string message_received = message(message_length);
string message_understood = accept_message();
if (message_received != message_understood) {
mistakes += 1;
switch (mistakes) {
case 1:
print_mistake_effect("The table begins to shake!");
break;
case 2:
print_mistake_effect("The light bulb shatters!");
break;
case 3:
print_mistake_effect("Oh, no! A pair of clammy hands grasps your neck!");
return;
}
} else {
score += message_length;
if (score >= MAX_SCORE) {
print_centered("Whew! The spirits have gone!", MESSAGES_Y);
print_centered("You live to face another day!", MESSAGES_Y + 1);
return;
}
}
}
}
// Main {{{1
// =============================================================================
void main() {
set_style(DEFAULT_INK);
clear_screen();
print_credits();
press_enter("\nPress the Enter key to read the instructions. ");
clear_screen();
print_instructions();
press_enter("\nPress the Enter key to start. ");
clear_screen();
play();
write("\n");
}
In Raku
# Seance
# Original version in BASIC:
# By Chris Oxlade, 1983.
# https://archive.org/details/seance.qb64
# https://github.com/chaosotter/basic-games
# This version in Raku:
# Copyright (c) 2024, Marcos Cruz (programandala.net)
# SPDX-License-Identifier: Fair
#
# Written on 2024-12-14.
#
# Last modified: 20241214T1104+0100.
# Terminal {{{1
# ==============================================================
constant $BLACK = 0;
constant $RED = 1;
constant $GREEN = 2;
constant $YELLOW = 3;
constant $BLUE = 4;
constant $MAGENTA = 5;
constant $CYAN = 6;
constant $WHITE = 7;
constant $DEFAULT = 9;
constant $STYLE_OFF = +20;
constant $FOREGROUND = +30;
constant $BACKGROUND = +40;
constant $BRIGHT = +60;
constant $NORMAL = 0;
constant $RESET_ALL = $NORMAL;
# Move the cursor to the home position.
#
sub move_cursor_home {
print "\e[H";
}
# Erase the screen.
#
sub erase_screen {
print "\e[2J";
}
# Set the color.
#
sub set_color(Int $color) {
print "\e[{$color}m";
}
# Reset the attributes.
#
sub reset_attributes {
set_color($RESET_ALL);
}
# Erase the screen, reset the attributes and move the cursor to the home position.
#
sub clear_screen {
erase_screen;
reset_attributes;
move_cursor_home;
}
# Erase from the current cursor position to the end of the current line.
#
sub erase_line_right {
print "\e[K";
}
# Erase the given line to the right of the given column.
#
sub erase_line_right_from(Int $line, Int $column) {
set_cursor_position($line, $column);
erase_line_right;
}
# Make the cursor invisible.
#
sub hide_cursor {
print "\e[?25l";
}
# Make the cursor visible.
#
sub show_cursor {
print "\e[?25h";
}
# Set the cursor position to the given coordinates (the top left position is 1, 1).
#
sub set_cursor_position(Int $line, Int $column) {
print "\e[$line;{$column}H";
}
# Globals {{{1
# =============================================================
constant $TITLE = 'Seance';
constant $MAX_SCORE = 50;
constant $MAX_MESSAGE_LENGTH = 6;
constant $MIN_MESSAGE_LENGTH = 3;
constant $BASE_CHARACTER = ord('@');
constant $PLANCHETTE = '*';
constant $FIRST_LETTER_NUMBER = 1;
constant $LAST_LETTER_NUMBER = 26;
constant $BOARD_INK = $FOREGROUND + $BRIGHT + $CYAN;
constant $DEFAULT_INK = $FOREGROUND + $WHITE;
constant $INPUT_INK = $FOREGROUND + $BRIGHT + $GREEN;
constant $INSTRUCTIONS_INK = $FOREGROUND + $YELLOW;
constant $MISTAKE_EFFECT_INK = $FOREGROUND + $BRIGHT + $RED;
constant $PLANCHETTE_INK = $FOREGROUND + $YELLOW;
constant $TITLE_INK = $FOREGROUND + $BRIGHT + $RED;
constant $MISTAKE_EFFECT_PAUSE = 3; # seconds
constant $BOARD_WIDTH = 8; # characters displayed on the top and bottom borders
constant $BOARD_PAD = 1; # blank characters separating the board from its left and right borders
constant $BOARD_ACTUAL_WIDTH = $BOARD_WIDTH + 2 * $BOARD_PAD; # screen columns
constant $BOARD_HEIGHT = 5; # characters displayed on the left and right borders
constant $BOARD_BOTTOM_Y = $BOARD_HEIGHT + 1; # relative to the board
constant $BOARD_X = 29; # screen column
constant $BOARD_Y = 5; # screen line
constant $INPUT_X = $BOARD_X;
constant $INPUT_Y = $BOARD_Y + $BOARD_BOTTOM_Y + 4;
constant $MESSAGES_Y = $INPUT_Y;
# Input {{{1
# =============================================================
# Print the given prompt and wait until the user enters a string.
#
sub input(Str $prompt = '' --> Str) {
set_color($INPUT_INK);
my $result = prompt $prompt;
set_color($DEFAULT_INK);
return $result;
}
# Board {{{1
# =============================================================
# Return the x coordinate to print the given text centered on the board.
#
sub board_centered_x(Str $text --> Int) {
return ($BOARD_X + ($BOARD_ACTUAL_WIDTH - $text.chars) / 2).Int;
}
# Print the given text on the given row, centered on the board.
#
sub print_board_centered(Str $text, Int $y) {
set_cursor_position($y, board_centered_x($text));
put $text;
}
# Print the title at the current cursor position.
#
sub print_title {
set_color($TITLE_INK);
put $TITLE;
set_color($DEFAULT_INK);
}
# Print the title on the given row, centered on the board.
#
sub print_board_centered_title(Int $y) {
set_color($TITLE_INK);
print_board_centered($TITLE, $y);
set_color($DEFAULT_INK);
}
# Info {{{1
# =============================================================
sub print_credits {
print_title;
put "\nOriginal version in BASIC:";
put ' Written by Chris Oxlade, 1983.';
put ' https://archive.org/details/seance.qb64';
put ' https://github.com/chaosotter/basic-games';
put '';
put 'This version in Raku:';
put ' Copyright (c) 2024, Marcos Cruz (programandala.net)';
put " SPDX-License-Identifier: Fair";
}
sub print_instructions {
print_title;
set_color($INSTRUCTIONS_INK);
put "\nMessages from the Spirits are coming through, letter by letter. They want you";
put 'to remember the letters and type them into the computer in the correct order.';
put 'If you make mistakes, they will be angry ― very angry…';
put '';
put "Watch for stars on your screen ― they show the letters in the Spirits'";
put 'messages.';
set_color($DEFAULT_INK);
}
# Board {{{1
# =============================================================
# Print the given character at the given board coordinates.
#
sub print_character(Int $y, Int $x, Str $a) {
set_cursor_position($y + $BOARD_Y, $x + $BOARD_X);
print $a;
}
sub print_board {
set_color($BOARD_INK);
for 1 .. $BOARD_WIDTH -> $i {
print_character(0, $i + 1, ($BASE_CHARACTER + $i).chr); # top border
print_character($BOARD_BOTTOM_Y, $i + 1, ($BASE_CHARACTER + $LAST_LETTER_NUMBER - $BOARD_HEIGHT - $i + 1).chr); # bottom border
}
for 1 .. $BOARD_HEIGHT -> $i {
print_character($i , 0, ($BASE_CHARACTER + $LAST_LETTER_NUMBER - $i + 1).chr); # left border
print_character($i , 3 + $BOARD_WIDTH, ($BASE_CHARACTER + $BOARD_WIDTH + $i).chr); # right border
}
put '';
set_color($DEFAULT_INK);
}
# Output {{{1
# =============================================================
# Print the given mistake effect, wait a configured number of seconds and
# finally erase it.
#
sub print_mistake_effect(Str $effect) {
my $x = board_centered_x($effect);
hide_cursor;
set_cursor_position($MESSAGES_Y, $x);
set_color($MISTAKE_EFFECT_INK);
put $effect;
set_color($DEFAULT_INK);
sleep $MISTAKE_EFFECT_PAUSE;
erase_line_right_from($MESSAGES_Y, $x);
show_cursor;
}
# Return a new message of the given length, after marking its letters on the
# board.
#
sub message(Int $length --> Str) {
my Int $y;
my Int $x;
my Str $message = '';
hide_cursor;
for 1 .. $length -> $i {
my $letter_number = ($FIRST_LETTER_NUMBER .. $LAST_LETTER_NUMBER).pick;
my $letter = ($BASE_CHARACTER + $letter_number).chr;
$message = "$message$letter";
given True {
when $letter_number <= $BOARD_WIDTH {
# top border;
$y = 1;
$x = $letter_number + 1;
}
when $letter_number <= $BOARD_WIDTH + $BOARD_HEIGHT {
# right border;
$y = $letter_number - $BOARD_WIDTH;
$x = 2 + $BOARD_WIDTH;
}
when $letter_number <= $BOARD_WIDTH + $BOARD_HEIGHT + $BOARD_WIDTH {
# bottom border;
$y = $BOARD_BOTTOM_Y - 1;
$x = 2 + $BOARD_WIDTH + $BOARD_HEIGHT + $BOARD_WIDTH - $letter_number;
}
default {
# left border;
$y = 1 + $LAST_LETTER_NUMBER - $letter_number;
$x = 1;
}
}
set_color($PLANCHETTE_INK);
print_character($y, $x, $PLANCHETTE);
set_color($DEFAULT_INK);
sleep 1;
print_character($y, $x, ' ');
}
show_cursor;
return $message;
}
# Accept a string from the user, erase it from the screen and return it.
#
sub message_understood(--> Str) {
set_cursor_position($INPUT_Y, $INPUT_X);
my $result = (prompt '? ').uc;
erase_line_right_from($INPUT_Y, $INPUT_X);
return $result;
}
# Main {{{1
# =============================================================
sub play {
my $score = 0;
my $mistakes = 0;
print_board_centered_title(1);
print_board;
loop {
my $message_length = ($MIN_MESSAGE_LENGTH .. $MAX_MESSAGE_LENGTH).pick;
if message($message_length) ne message_understood() {
$mistakes += 1;
given $mistakes {
when 1 {
print_mistake_effect('The table begins to shake!');
}
when 2 {
print_mistake_effect('The light bulb shatters!');
}
when 3 {
print_mistake_effect('Oh, no! A pair of clammy hands grasps your neck!');
return;
}
}
} else {
$score += $message_length;
if $score >= $MAX_SCORE {
print_board_centered('Whew! The spirits have gone!', $MESSAGES_Y);
print_board_centered('You live to face another day!', $MESSAGES_Y + 1);
return;
}
}
}
}
set_color($DEFAULT_INK);
clear_screen;
print_credits;
prompt "\nPress the Enter key to read the instructions. ";
clear_screen;
print_instructions;
prompt "\nPress the Enter key to start. ";
clear_screen;
play;
put "\n";
In V
/*
Seance
Original version in BASIC:
By Chris Oxlade, 1983.
https://archive.org/details/seance.qb64
https://github.com/chaosotter/basic-games
This version in V:
Copyright (c) 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written on 2025-01-08.
Last modified: 20250122T0343+0100.
*/
import os
import rand
import strconv
import term
import time
// Console {{{1
// =============================================================
const black = 0
const red = 1
const green = 2
const yellow = 3
const blue = 4
const magenta = 5
const cyan = 6
const white = 7
const default_color = 9
const foreground = 30
const background = 40
const bright = 60
// Set the given color.
//
fn set_color(color int) {
print('\x1B[0;${color}m')
}
// Erase the given line to the right of the given column.
//
fn erase_line_toend_from(y int, x int) {
term.set_cursor_position(y: y, x: x)
term.erase_line_toend()
}
// Global constants {{{1
// =============================================================
const title = 'Seance'
const max_score = 50
const max_message_length = 6
const min_message_length = 3
const base_character = int(`@`)
const planchette = `*`
const first_letter_number = 1
const last_letter_number = 26
const board_ink = bright + cyan + foreground
const default_ink = default_color + foreground
const input_ink = bright + green + foreground
const instructions_ink = yellow + foreground
const mistake_effect_ink = bright + red + foreground
const planchette_ink = yellow + foreground
const title_ink = bright + red + foreground
const input_x = board_x
const input_y = board_y + board_bottom_y + 4
const messages_y = input_y
const mistake_effect_pause = 3 // seconds
const board_actual_width = board_width + 2 * board_pad // screen columns
const board_bottom_y = board_height + 1 // relative to the board
const board_height = 5 // characters displayed on the left and right borders
const board_pad = 1 // blank characters separating the board from its left and right borders
const board_width = 8 // characters displayed on the top and bottom borders
const board_x = 29 // screen column
const board_y = 5 // screen line
// User input {{{1
// =============================================================
// Print the given prompt and wait until the user enters a string.
//
fn input(prompt string) string {
set_color(input_ink)
defer { set_color(default_ink) }
return os.input(prompt)
}
// Print the given prompt and wait until the user presses Enter.
//
fn press_enter(prompt string) {
input(prompt)
}
// Return the x coordinate to print the given text centered on the board.
//
fn board_centered_x(text string) int {
return board_x + (board_actual_width - text.len) / 2
}
// Print the given text on the given row, centered on the board.
//
fn print_board_centered(text string, y int) {
term.set_cursor_position(y: y, x: board_centered_x(text))
println(text)
}
// Title, credits and instructions {{{1
// =============================================================
// Print the title at the current cursor position.
//
fn print_title() {
set_color(title_ink)
println(title)
set_color(default_ink)
}
fn print_credits() {
print_title()
println('\nOriginal version in BASIC:')
println(' Written by Chris Oxlade, 1983.')
println(' https://archive.org/details/seance.qb64')
println(' https://github.com/chaosotter/basic-games')
println('\nThis version in V:')
println(' Copyright (c) 2025, Marcos Cruz (programandala.net)')
println(' SPDX-License-Identifier: Fair')
}
fn print_instructions() {
print_title()
set_color(instructions_ink)
defer { set_color(default_ink) }
println('\nMessages from the Spirits are coming through, letter by letter. They want you')
println('to remember the letters and type them into the computer in the correct order.')
println('If you make mistakes, they will be angry -- very angry...')
println('')
println("Watch for stars on your screen -- they show the letters in the Spirits'")
println('messages.')
}
// Game {{{1
// =============================================================
// Print the given letter at the given board coordinates.
//
fn print_char(y int, x int, a rune) {
term.set_cursor_position(y: y + board_y, x: x + board_x)
print(a)
}
fn print_board() {
set_color(board_ink)
defer { set_color(default_ink) }
for i := 1; i <= board_width; i++ {
print_char(0, i + 1, rune(base_character + i)) // top border
print_char(board_bottom_y, i + 1, rune(base_character +
last_letter_number - board_height - i + 1)) // bottom border
}
for i := 1; i <= board_height; i++ {
print_char(i, 0, rune(base_character + last_letter_number - i + 1)) // left border
print_char(i, 3 + board_width, rune(base_character + board_width + i)) // right border
}
println('')
}
// Print the title on the given row, centered on the board.
//
fn print_board_centered_title(y int) {
set_color(title_ink)
print_board_centered(title, y)
set_color(default_ink)
}
// Wait the given number of seconds.
//
fn wait_seconds(seconds int) {
time.sleep(seconds * time.second)
}
// Print the given mistake effect, wait a configured number of seconds and
// finally erase it.
//
fn print_mistake_effect(effect string) {
x := board_centered_x(effect)
term.hide_cursor()
term.set_cursor_position(y: messages_y, x: x)
set_color(mistake_effect_ink)
println(effect)
set_color(default_ink)
wait_seconds(mistake_effect_pause)
erase_line_toend_from(messages_y, x)
term.show_cursor()
}
// Return a new message of the given length, after marking its letters on the
// board.
//
fn message(length int) string {
mut y := 0
mut x := 0
mut message := ''
term.hide_cursor()
for i := 1; i <= length; i++ {
letter_number := rand.i32_in_range(first_letter_number, last_letter_number + 1) or {
first_letter_number
}
letter := rune(base_character + letter_number)
unsafe {
message = strconv.v_sprintf('${message}${letter}') // add letter to message
}
match true {
letter_number <= board_width {
// top border
y = 1
x = letter_number + 1
}
letter_number <= board_width + board_height {
// right border
y = letter_number - board_width
x = 2 + board_width
}
letter_number <= board_width + board_height + board_width {
// bottom border
y = board_bottom_y - 1
x = 2 + board_width + board_height + board_width - letter_number
}
else {
// left border
y = 1 + last_letter_number - letter_number
x = 1
}
}
set_color(planchette_ink)
defer { set_color(default_ink) }
print_char(y, x, planchette)
println('')
wait_seconds(1)
print_char(y, x, ` `)
}
term.show_cursor()
return message
}
// Accept a string from the user, erase it from the screen and return it.
//
fn message_understood() string {
term.set_cursor_position(y: input_y, x: input_x)
defer { erase_line_toend_from(input_y, input_x) }
return input('? ').to_upper()
}
fn play() {
mut score := 0
mut mistakes := 0
print_board_centered_title(1)
print_board()
for {
message_length := rand.i32_in_range(min_message_length, max_message_length + 1) or {
first_letter_number
}
if message(message_length) != message_understood() {
mistakes += 1
match mistakes {
1 {
print_mistake_effect('The table begins to shake!')
}
2 {
print_mistake_effect('The light bulb shatters!')
}
3 {
print_mistake_effect('Oh, no! A pair of clammy hands grasps your neck!')
return
}
else {}
}
} else {
score += message_length
if score >= max_score {
print_board_centered('Whew! The spirits have gone!', messages_y)
print_board_centered('You live to face another day!', messages_y + 1)
return
}
}
}
}
// Main {{{1
// =============================================================
fn main() {
set_color(default_ink)
term.clear()
print_credits()
press_enter('\nPress the Enter key to read the instructions. ')
term.clear()
print_instructions()
press_enter('\nPress the Enter key to start. ')
term.clear()
play()
println('\n')
}
