Fiction!

What is Fiction! ?

It is a new text-based adventure game. It is a small, object-oriented, but full-featured interactive fiction engine.

See also A text adventure game engine

It makes use of features of Tcl 8.5, and it has no dependency on Tk, as it is text-only. It supports a simple I18N mechanism. It requires Basique - OO-like namespaces. Just put the source file named basique.tcl in the directory of Fiction! and it will work.


Sarnold is the creator of this software. It is delivered under a BSD-like license, like Tcl.


2008-07-28: Sarnold added the ability to save and restore games.

2008-11-09: Refactored a bit this script and added an english demo. It is ready to use!


TODO: Use msgcat for i18n.


The code

#!/usr/bin/env tclsh

package require Tcl 8.5
# the OO layer
source basique.tcl

# bootstrapping
set debug yes
# the standalone demo version
set demo yes

# I18N
# Change this to "en" if you want messages in English
set lang fr
if $demo {set lang en}

proc assert {expr {msg "assertion failed"}} {if {![uplevel 1 [list expr $expr]]} {error $msg}}
proc iassert {expr {msg "assertion failed"}} {if {![uplevel 1 [list expr $expr]]} {error [i $msg]}}
proc ? {bool iftrue iffalse} {
        if {$bool} {return $iftrue}
        set iffalse
}
proc way? {to} {
        set to [string tolower $to]
        set to [string map {south s north n west w east e} $to]
        iassert [regexp {[senw]|s[ew]|n[ew]|e[ns]|w[ns]} $to] "invalid direction"
        set to
}
# normalizes a namespace
proc nsnorm val {
        namespace eval $val namespace current
}
proc nseq {a b} {
        string equal [nsnorm $a] [nsnorm $b]
}

proc i {s} {
        if {$::lang eq "fr" && [info exists ::msg($s)]} {
                return $::msg($s)
        }
        set s
}

proc iputs {s} {warn [i $s]}


# ack et fail: shorthands to return true and return false
interp alias {} ack {} return true
interp alias {} fail {} return false
# a useful proc: removes an element from a list,
# even with multiple elements
proc lremove {list elt} {
        while {[set i [lsearch -exact $list $elt]]>=0} {
                set list [lreplace $list $i $i]
        }
        set list
}
        

basique::class Room {
        variable name
        variable ways
        variable objs ""
        variable description
        proc __init__ {desc args} {
                # do not change set$opt to set $opt
                # because this is a dispatched command
                set-name [self]
                set-description $desc
                foreach {opt val} $args {[self]::set$opt $val}
        }
        proc __destroy__ {} {
                foreach o [-> objs] {$o destroy}
        }
        proc seek {seek} {
                instance
                set res ""
                foreach o $objs {
                        if {[$o match $seek]} {
                                lappend res $o
                        }
                }
                set res
        }
        proc watch {} {
                instance
                foreach o $objs {$o watch}
        }
        proc add {what} {
                lappend [. objs] $what
        }
        proc remove {a} {
                instance
                set objs [lremove $objs $a]
        }
        proc set-description val {set [. description] $val}
        proc set-name val {set [. name] $val}
        proc ways {to name} {
                set to [way? $to]
                variable ways
                dict set ways $to $name
        }
        proc where {way} {
                variable ways
                dict get $ways [way? $way]
        }
        proc object {args} {
                Object new {*}$args -place [self]
        }
        proc person {args} {
                Person new {*}$args -place [self]
        }
        proc here {} {
                warn [-> description]
                dict for {dir room} [-> ways] {
                        set dir [join [split $dir ""] -]
                        # set direction in english
                        set dir [string map {n north w west e east s south} $dir]
                        # i18n
                        warn [format [i [format "There is a way to the %s to %%s." $dir]] [$room -> name]]
                }
        }
        variable leave StdLeave
        proc StdLeave {} {
                warn "[i "You leave"] [-> name]."
        }
        proc Leave {} {eval [-> leave]}
}
                
basique::class Object {
        variable name
        variable use ""
        variable take {ack "you take the object"}
        variable act fail
        variable place Home
        variable desc "No desc"
        proc __init__ {desc args} {
                set-name [self]
                set [. desc] $desc
                foreach {opt val} $args {[self]::set$opt $val}
        }
        proc __destroy__ {} {[-> quality] destroy}
        proc in val {nseq $val [-> place]}
        proc set-place val {
                [-> place] remove [self]
                set [. place] $val
                $val add [self]
        }
        proc set-name val {set [. name] $val}
        proc match seek {string match -nocase $seek [-> name]}
        proc use {onwhat body} {dict set [. use] [nsnorm $onwhat] $body}
        proc Use {onwhat} {
                variable use
                set onwhat [nsnorm $onwhat]
                if {[dict exists $use $onwhat]} {
                        eval [dict get $use $onwhat]
                } else {
                        iputs "Nothing happens."
                }
        }
        proc take {val msg} {set [. take] [handler $val $msg]}
        proc Take {} {handle [-> take] }
        proc act {val msg} {set [. act] [handler $val $msg]}
        proc Act {} {handle [-> act]}
        proc watch {} {iputs "[-> desc] ([-> name])"}
}

# some small procs to handle message and body of a handler
proc handler {val msg} {
        list $val $msg
}
proc handle {n} {
        iputs [lindex $n 1]
        uplevel 1 [lindex $n 0]
}
basique::class Person {
        variable name
        variable inventory ""
        variable talk {default dummy}
        variable place Home
        variable state default
        variable description
        proc __init__ {args} {
                set-name [self]
                foreach {opt val} $args {[self]::set$opt $val}
        }
        proc set-state val {set [. state] $val}
        proc set-place val {
                [-> place] remove [self]
                set [. place] $val
                $val add [self]
        }
        proc watch {} {
                iputs [-> description]
        }
        proc gives {person obj} {
                $obj set-place $person
        }
        proc set-description val {set [. description] $val}
        proc The {} {string trim "[phrase The] [-> name]"}
        proc set-name val {
            set [. name] $val
            set-description [format [i "Here is %s."] [-> name]]
        }
        proc match seek {string match $seek [-> name]}
        proc set-talk val {talk $val}
        proc talk args {
                assert {[llength $args]>0}
                set val [lindex $args end]
                set state [getopts [lrange $args 0 end-1] -state default]
                dict set [. talk] $state $val
        }
        proc says {txt} {warn "[-> name]: $txt"}
        
        proc object {args} {
                lappend [. inventory] [Object new {*}$args]
        }
        proc Talk {args} {eval [dict get [-> talk] [-> state]]}
        proc dummy {} {
                warn [random [i "Talk less and act much!"] [i "Nothing happens."]]
        }
}

basique::class Ego {
        variable inventory ""
        variable place
        variable history ""
        variable score 0
        proc __init__ {where} {puts {Fiction! (c) Stephane Arnold 2008
BSD-like Tcl license (see https://www.tcl-lang.org/)}
                set [. place] $where
        }
        proc __destroy__ {} {
                foreach o [-> inventory] {$o destroy}
                [-> place] destroy
        }
        proc register {args} {
                switch -- [lindex $args 0] {
                        Save {}
                        default {lappend [. history] $args}
                }
        }
        proc score {n} {incr [. score] $n}
        proc WinTheGame {} {
                iputs Congratulations!
                PutScore
        }
        proc LoseTheGame {} {
                iputs Sorry...
                PutScore
        }
        proc Save {args} {
                warn [format [i "This will save the current game to '%s'"] $args]
                puts -nonewline [i "Are you sure ? (y/n) "]
                flush stdout
                gets stdin line
                if {![string equal $line [i y]]} {
                        iputs "Save aborted."
                        return
                }
                if {[catch {
                        set fd [open $args w]
                        # we write the history to a file
                        foreach l [-> history] {puts $fd $l}
                        close $fd
                } msg]} {
                        error [format [i "Cannot write to '%s': %s"] $args $msg]
                }
                iputs "Game saved"
        }                        
        proc say {txt} {warn "[i I]: $txt"}
        proc match {match} {
                set res ""
                foreach o [-> inventory] {
                        if {[$o match *$match*]} {lappend res $o}
                }
                concat $res [[-> place] seek *$match*]
        }
        proc seekN {n list} {
                if {$n == 1} {return [seek $list]}
                set i [llength $list]
                incr i -1
                while {[llength [match [lrange $list 0 $i]]]!=1} {
                        incr i -1
                        if {$i < 0} {error "[i "cannot find object or person"]: $list"}
                }
                concat [match [lrange $list 0 $i]] [seekN [incr n -1] [lrange $list [expr {$i+1}] end]]
        }
        proc seek {list} {
                # seek for an object or a person matching a list of strings
                set match [match $list]
                if {[llength $match] == 0} {
                        error "[i "cannot find object or person"]: $list"                        
                }
                if {[llength $match]>1} {return [probe $match]}
                return [lindex $match 0]
        }
        proc probe list {
                iputs "Did you mean?"
                fori i x $list {warn "[expr {$i+1}]: [$x -> name]"}
                while yes {
                        warn -nonewline "1-[llength $list]? "
                        flush stdout
                        gets stdin answer
                        if {![string is integer $answer]} { 
                                iputs "must be an integer"
                                continue
                        }
                        if {$answer >0 && $answer <= [llength $list]} {
                                return [lindex $list [expr {$answer -1}]]
                        }
                }
                # dead end
        }
        proc Take {args} {
                grab [seek $args]
        }
        proc Drop {args} {
                drop [seek $args]
        }
        proc PutScore {} {warn [format [i "Your score: %d points."] [-> score]]}
        proc Inv {} {
                PutScore
                if {[llength [-> inventory]]==0} {
                        iputs "Nothing in your pockets."
                        return
                }
                iputs "In your pockets:"
                foreach i [-> inventory] {puts "- [$i -> name]"}
        }
        proc Look {} {
                [-> place] here
                [-> place] watch
        }
        proc Help {} {
                puts [set ::Help_$::lang]
        }
        proc Exit {} {
            puts -nonewline [i "Are you sure you want to quit ? (y/n) "]
            flush stdout
            gets stdin c
            if {[string equal $c [i y]]} exit
        }
        proc Go {way} {
                variable place
                if {[catch {set where [$place where $way]}]} {
                        error [i "There is no way like this"]
                }
                $place Leave
                $where here
                set place $where
        }
        proc grab {what} {
                iassert {![$what in [self]]} "I already have it."
                assert {[$what in [-> place]]} "[i "cannot take"] [$what -> name]."
                assert [$what Take] "[i "cannot take"] [$what -> name]."
                $what set-place I
        }
        proc add val {
                lappend [. inventory] [nsnorm $val]
        }
        proc drop {what} {
                puts "[i "You drop"] [$what -> name]"
                $what set-place [-> place]
        }
        # to talk with people
        proc Talk {args} {
            [seek $args] Talk
        }
        proc remove {what} {
                instance
                assert {[lsearch -exact $inventory $what]>=0} "[i "no such object"]: $what"
                set inventory [lremove $inventory $what]
        }
        proc Use {args} {
                eval use [seekN 2 $args]
        }
        proc use {what onwhat} {
                assert {[$what in [self]]} "[i "I do not have"] [$what -> name]."
                $what Use $onwhat
        }
        proc act {what} {
                assert {[$what in [self]] || [$what in [-> place]]} "[i "cannot use"] [$what -> name]."
                $what Act
        }
        proc Act {args} {act [seek $args]}
}

# returns a random element within arguments
proc random {args} {
        lindex $args [expr {int(rand()*[llength $args])}]
}
# manage options
proc getopts {alist opt default} {
        if {[llength $alist] == 0} {
                return $default
        }
        assert {[llength $alist]%2 == 0}
        foreach {key val} $alist {
                if {$key eq $opt} {return $val}
        }
        return $default
}

proc fori {x var lst body} {
        uplevel 1 set $x 0
        uplevel 1 [list foreach $var $lst "$body\nincr $x"]
}

interp alias {} warn {} puts
proc Direction {s} {
        set s [Direction_$::lang $s]
        iassert [regexp {^([nsew]|n[we]|s[ew]|w[ns]|e[ns])$} $s] "invalid direction"
        list Go $s
}
proc Direction_en {s} {
        string map {- "" north n east e west w south s} $s
}
proc Direction_fr s {
        string map {est e o w} [string map {- "" nord n ouest o sud s} $s]
}
proc cmd {word} {
        if {![catch {
                set dir [Direction $word]
        }]} {
                return $dir
        }
        set commands [dict create fr {
                quitter Exit
                exit Exit
                laisser Drop
                prendre Take
                parler Talk
                utiliser Use
                actionner Act
                regarder Look
                va Go
                aller Go
                inventaire Inv
                aider Help
                sauver Save
        } en {
                quit Exit
                exit Exit
                drop Drop
                take Take
                talk Talk
                use Use
                act Act
                look Look
                go Go
                inventory Inv
                help Help
                save Save
        }]
        set res ""
        foreach {name cmd} [dict get $commands $::lang] {
                if {[string match $word* $name]} {
                        lappend res $cmd
                }
        }
        if {[llength $res]==1} {return [lindex $res 0]}
        error "I do not understand"
}
proc dummy {word} {
        expr {[lsearch [set ::dummy_$::lang] $word]>=0}
}
set dummy_en {a the on with}
set dummy_fr {à un une la le les des sur avec}
# set printing to stdout on/off
proc Warn {bool} {
        if {$bool} {
                proc warn {s} {puts $s; after 300}
        } else {
                proc warn {s} {}
        }
}

set Help_en {Commands are:
quit/exit        Exit the game
n-s-w-e                Go to the north, south...
take obj        Take an object
drop obj        Drop an object
look                Take a look upon your environment
act obj                Activate an object
talk person        Talk to a person
use obj what        Use an object on something/someone
go n-s-w-e        Go to the north, south...}

set Help_fr {Voici les ordres que je comprends :
quitter/exit                Termine le jeu
n-s-o-e                        Se déplacer au nord, sud,....
prendre obj                Prendre un objet
laisser obj                Lacher un objet
regarder                Regarder autour de soi
actionner obj                Actionner un objet
parler personne                Parler à quelqu'un
utiliser obj chose        Utiliser un objet sur qq chose ou qq'un
go n-s-o-e                Se déplacer}
proc main {{file ""}} {
        if {$file ne ""} {
                puts [format [i "Restoring data from %s..."] $file]
                Warn off
                if {[catch {
                        set i 0
                        set fd [open $file]
                        while {![eof $fd]} {
                                gets $fd line
                                if {$line ne ""} {
                                        catch {eval I $line}
                                        eval I register $line
                                }
                                # display a progress indicator
                                incr i
                                if {$i % 100 == 0} {puts -nonewline .}
                        }
                        close $fd
                } msg info]} {
                        puts $msg
                        puts stderr $info
                        exit
                }
                Warn on
        }
        I Look
        while 1 {
                puts -nonewline "> "
                flush stdout
                gets stdin line
                if {[catch {cmd [lindex $line 0]} cmd]} {
                        iputs "I do not understand, please type help"
                        continue
                }
                # filters dummy words (to, a, the, ...)
                # in any language
                set res [list]
                foreach i [lrange $line 1 end] {
                        if {![dummy $i]} {lappend res $i}
                }
                if {[catch {eval I $cmd $res} msg info]} {
                        puts $msg
                        if {$::debug} {puts $info}
                } else {
                        eval I register $cmd $res
                }
        }
}

# I18N en Francais
array set msg {
        "assertion failed" "échec de contrôle de données"
        "Restoring data from %s..." "Restauration de la partie stockée dans %s..."
        "Talk less and act much!" "Moins de paroles, plus d'action !"
        "too many words" "trop de mots dans la commande"
        "You leave" "Vous quittez"
        "He is" "C'est un"
        "Here is" "Il y a"
        "This will save the current game to '%s'" "Cela enregistrera dans '%s' la partie en cours."
        "Game saved" "Jeu sauvegardé"
        "Are you sure ? (y/n) " "Êtes-vous sûr ? (o/n) "
        "You take" "Vous prenez"
        "Nothing happens." "Rien ne se passe."
        "There is a way to the north to %s." "Il y a une voie au nord vers %s."
        "There is a way to the south to %s." "Il y a une voie au sud vers %s."
        "There is a way to the west to %s." "Il y a une voie à l'ouest vers %s."
        "There is a way to the east to %s." "Il y a une voie à l'est vers %s."
        north nord south sud west ouest east est 
        "There is no way like this" "Il n'y a pas de voie dans cette direction."
        "You drop" "Vous déposez" I Moi
        "There is" "Il y a"
        "use %s on what?" "Sur quoi voulez-vous utiliser %s ?"
        "Your score: %d points." "Votre score est de %d points."
        "In your pockets:" "Dans tes poches :"
        "Nothing in your pockets." "Tu n'as rien dans les poches."
        "I do not understand, please type help" "Je ne comprends pas. (tape aide)"
        "Are you sure you want to quit ? (y/n) " "Etes-vous sûr de vouloir quitter ce programme ? (o/n) "
        y o
        "Cannot write to '%s': %s" "Impossible d'accéder à '%s' : %s"
        "cannot find object or person" "je ne trouve pas cet objet ou cette personne"
        "Here is %s." "Il y a %s."
}


## 
 #  Testing material
 ##
if $demo {
    Room new Home {Home, sweet home...} -name {your home} 
    Home ways n Garden
    Room new Garden {You stand in a wonderful garden with flowers of all essences.} -name "the garden"
    Home object Rasoir {There is a razor that is really dirty.} -name "a razor"
    Garden ways s Home
    Rasoir take ack {You take the razor.}
    Rasoir act ack {You are shaving. Mmmh, this is nice.}
    Garden object Flower {A beautiful rose.} -name "a flower"
    Flower take ack {You take this rose.}
    Garden person Claire -name "a girl" -state unknown
    Claire talk -state unknown {
        I say "Hi! How do you do?"
        Claire says "I'm Claire. How do you do?"
        I say "I'm Julien. Nice to meet you!"
        Claire says "Nice to meet you, Julien!"
        Claire set-name Claire
        Claire set-state default
    }        
    Claire talk -state default dummy
    Flower use Claire {
        Claire says "Oh, thanks a lot! I love these flowers."
    }
} else {source aventure.txt}
## End of material  ##



Ego new I Home
eval main $argv