Initial commit

This commit is contained in:
Robert Miles 2020-06-29 21:38:45 -04:00
commit 40e0a2aab0
8 changed files with 651 additions and 0 deletions

139
.gitignore vendored Normal file
View File

@ -0,0 +1,139 @@
# Created by https://www.toptal.com/developers/gitignore/api/python
# Edit at https://www.toptal.com/developers/gitignore?templates=python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# End of https://www.toptal.com/developers/gitignore/api/python

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# ChooseScript - CYOA-style Scripting
ChooseScript is a scripting language for CYOA-like stories. It has variables, input, jumping, and most importantly, it has choices!

146
SPEC.md Normal file
View File

@ -0,0 +1,146 @@
# ChooseScript Specification v0.1
## Abstract
ChooseScript is a simple scripting language for Choose Your Own Adventure-style
stories. It has facilities to handle rudimentary state, take input, give
output, make choices, and direct the story.
The reference implementation of ChooseScript was written in about a day in
Python. It should not be taken as a be-all-end-all guide for implementing
ChooseScript in any other context. Rather, when this doc and the reference
implementation contradict each other, this doc should be followed.
ChooseScript files SHOULD be stored with the extension "chs". They MAY be
stored with the extension "txt".
## 1. Lexer
### 1.1. Tokens
There are 6 types of tokens. They are as follows. (Note that each token name
can be used in a sentence to refer to a token of that type; for instance, a
`STEM` token may be referred to as simply "a stem".)
- `STEM` - A stem is defined as "a string of alphanumeric characters, starting
with a letter of either case, and without quotes."
- `BOOLEAN` - The literal stems `true` and `false`.
- `COMMAND` - A command. (See section 1.2 for commands.) All commands are
valid stems.
- `TARGET` - A stem, followed by a colon. This MUST NOT be the same as a
command, and it SHOULD be unique. Targets are used as jumping places for
`goto`, `beq`, and `bne` commands.
- `NUMBER` - An integer.
- `STRING` - A series of characters contained in double quotes. Double quotes
can be escaped in the string by the literal `\"`. Newlines and whitespace
inside strings are taken as literal.
### 1.2. Command stems
The following literal stems are commands. They MUST NOT be used as targets.
- `print` - see section 2.1
- `goto` - see section 2.2
- `beq` - see section 2.2.1
- `bne` - see section 2.2.2
- `choose` - see section 2.3
- `set` - see section 2.4
- `input` - see section 2.4.1
- `testequals` - see section 2.5
- `check` - see section 2.5.1
## 2. Commands
The current version of the ChooseScript spec implements 9 commands. Commands
can take 0 or more arguments (in practice, they each take at least one).
### 2.1. `print <STRING>`
Prints the argument string to an output. The argument string MUST expand
variables (see section 3.1, "Expanding variables in strings").
### 2.2. `goto <STEM>`
Execution resumes from the target corresponding to the argument stem. If there
is no target corresponding to the argument stem, an error MUST be thrown.
#### 2.2.1. `beq <STEM>`
If the flag is set (see section 3.2, "Comparison flag"), then this acts as a
`goto` command. Otherwise, execution MUST fall through to the next command.
#### 2.2.1. `bne <STEM>`
If the flag is clear, then this acts as a `goto` command. Otherwise, execution
MUST fall through to the next command.
### 2.3. `choose <STRING> <STEM> [<STRING> <STEM> ...]`
Allows the user to choose between 1 or more options. Each argument string
corresponds to an argument stem. If the option corresponding to an argument
string is chosen, that string's respective argument stem is used as the target
to resume execution at. If an invalid option is chosen, execution MUST fall
through to the next command. This allows the script to display a prompt to
choose again, or take a default action if possible.
### 2.4. `set <STEM> <STRING>` OR `set <STEM> <NUMBER>` OR `set <STEM>
<BOOLEAN>`
Sets the variable represented by the argument stem to the argument string,
number, or boolean.
#### 2.4.1 `input <STEM> <STRING> [<STRING>]`
Takes input from the user, and stores it in the variable represented by the
argument stem. The first argument string is used as the prompt, while the
second argument string is the error message to provide if no input is given.
If the user doesn't provide any input, the second argument string is shown, and
the user is prompted again. If the second argument string is not provided, a
message similar in intent to "You must provide a value!" MUST be used instead.
### 2.5. `testequals <STEM> <STRING>` OR `testequals <STEM> <NUMBER>` OR
`testequals <STEM> <BOOLEAN>`
Compares the variable represented by the argument stem to the argument string,
number, or boolean, and stores the result in the flag (see section 3.2,
"Comparison flag").
An undefined variable MUST fail all comparisons.
#### 2.5.1. `check <STEM>`
A shorthand for `testequals <STEM> true`. If the variable represented by the
argument stem is not a boolean value, an error MUST be thrown.
## 3. Implementation details
The following are details of the implementation. These are meant to be
generically-described concepts that describe a given function.
### 3.1. Expanding variables in strings
When a string is printed to an output, it must expand any variables in the
string. Variables are inserted into a string by surrounding the variable name
in double-curly brackets (like `{{so}}`).
If a variable is undefined, the insertion statement MUST remain unchanged. This
allows a script writer to notice when a variable hasn't been set.
If a variable is defined, the insertion statement MUST be replaced by the
string representation of the variable value. Booleans MUST be represented as
literal `true` and `false`.
### 3.2. Comparison flag
The `testequals` and `check` commands test a variable value. If that test
succeeds (i.e; the variable is equal to the given value, or the boolean
variable is true), the comparison flag MUST be set to true. If the test fails
(i.e; the variable is *not* equal to the given value, the boolean variable is
false, or the variable is undefined), the comparison flag MUST be set to false.
If the comparison flag is true, then `beq` will branch, and `bne` will allow
execution to fall through to the next command. If the comparison flag is false,
the opposite occurs.
The comparison flag MUST start as false.

142
altogether.chs Normal file
View File

@ -0,0 +1,142 @@
#Ask for name#
input name "What's your name?" "Just put `Evan` if you don't want to give me your real name. Or maybe `Norman`. I like Norman."
#Variables in {{double curly brackets}} get expanded if they're set.#
print "Hello, {{name}}! Welcome to the ChooseScript demo."
print "This is just a simple example of a game in ChooseScript. There's not much to be said."
#Choices are implemented via the choose command. The string is a label, and the stem is the goto target for that choice.#
menu:
choose "New Game" start "Continue" continue
#If the choice isn't valid, it'll fall through, so you can handle that however you please.#
print "Invalid choice!"
goto menu
#A simple password system can be implemented by checking against a table of known passwords.#
#The `testequals` command tests if a variable is equal to a value. If it is, then it sets the flag to true.#
#`beq` jumps to the specified target if the flag is true, and falls through otherwise.#
continue:
input password "Enter password."
testequals password "1234"
beq start
testequals password "5142"
beq markread
testequals password "6245"
beq turn1
testequals password "9142"
beq markreadturn1
testequals password "9999"
beq turn2
testequals password "8888"
beq markreadturn2
print "Invalid password {{password}}!"
print "Starting new game..."
start:
print "{{name}} wakes up in a room with no recollection of how they got there."
print "A plaque in front of them reads as follows:"
print "+---------------------------+"
print "| THE HALLWAY OF FATE |"
print "|Choose wisely, or you shall|"
print "| meet a death by endless |"
print "| abyss. |"
print "+---------------------------+"
choice:
print "There's a map on the wall to the left, and a hallway to the right."
choose "Read the map" map "Go down the hallway" turn1 "Quit the game" quitstart
#Boolean flags can be implemented using the `check` and `set` commands.#
#The relevant bit here is to check a flag by using the `check` command.#
#`check <var>` is a shortcut for `testequals <var> true`.#
#Don't use `check <var>` if the variable in question isn't a boolean.#
#Unset variables are assumed to be false.#
map:
check readmap
beq skipmap
print "From reading the map, you know you have to make a right turn, then go straight."
markread:
#To set a flag, use `set <var> true`. (Or `set <var> false` to clear a flag.)#
set readmap true
goto choice
skipmap:
print "You already read the map. Just go down the hallway."
goto start
#The `markread<blah>` labels simply set the readmap flag and fall through to their respective label.#
markreadturn1:
set readmap true
turn1:
print "You come to a 3-way fork in the hallway. Do you:"
choose "Go left" fail "Go right" turn2 "Go straight" fail "Quit the game" quitturn1
print "Invalid choice!"
goto turn1
markreadturn2:
set readmap true
turn2:
print "You turn to the right, and breathe a sigh of relief as the floor doesn't drop out from under you."
check readmap
bne turn2choose
print "After all, that's what the map said to do."
turn2choose:
print "A ways down the hallway, you reach another 3-way fork. Do you:"
choose "Go left" fail "Go right" fail "Go straight" success "Quit the game" quitturn2
print "Invalid choice!"
goto turn2choose
#Generalized failure message. I could have a different failure message for each#
#possible wrong direction, but I don't feel like putting that much effort in.#
fail:
print "You pick your path. As you go to walk down it, you suddenly feel the floor drop out from under you. You fall into an endless abyss."
print "Game over."
goto end
#Success condition.#
success:
check readmap
bne skipendmap
print "The map said to go straight, and so straight you went."
skipendmap:
print "As you head straight down the hallway, you breathe a sigh of relief. The floor under you stays solid. Soon, you're blinded by a light."
print "When the light clears, you're back in front of your computer."
print "You did it! Congratulations, {{name}}!"
#The flag is still set to the value of `readmap` from earlier, so we don't need to `check` it again.#
beq end
print "You didn't even need the map! Did you cheat, or did you trial-and-error your way through it?"
print "Either way, most excellent!"
goto end
#Now for the password system. #
#At every stage, you can quit out, and you'll receive a password. #
#For each stage, the corresponding `quit<label>` checks if readmap #
#is set, and if so, it jumps to `quit<label>read`. Otherwise, it spits#
#out the password for not having read the map and quits. #
#If readmap is set, however, it'll spit out the password for having #
#read the map instead. #
quitstart:
check readmap
beq quitstartread
print "Your password is `1234`."
goto end
quitstartread:
print "Your password is `5142`."
goto end
quitturn1:
check readmap
beq quitturn1read
print "Your password is `6245`."
goto end
quitturn1read:
print "Your password is `9142`."
goto end
quitturn2:
check readmap
beq quitturn2read
print "Your password is `9999`."
goto end
quitturn2read:
print "Your password is `8888`."
goto end
#The end label is at the end of the file, and serves as a way to skip everything. #
#It doesn't necessarily have to be called `end`, but I call it that because that's#
#what it is; the end. #
end:

174
choosescript.py Normal file
View File

@ -0,0 +1,174 @@
from sly import Lexer as SlyLexer
commands = ["goto","print","choose","input","set","testequals","check","beq","bne"]
class Lexer(SlyLexer):
tokens = { STEM, COMMAND, TARGET, STRING, NUMBER, BOOLEAN }
ignore = ' \t'
TARGET = r'([A-Za-z][A-Za-z0-9]*):'
def TARGET(self,t):
t.value = t.value.strip()[:-1]
assert t.value not in commands, f"Cannot use name of command {t.value} as branch/goto target"
return t
BOOLEAN = r'(true|false)'
def BOOLEAN(self,t):
t.value = t.value=="true"
return t
COMMAND = '('+'|'.join(commands)+')'
STEM = r'([A-Za-z][A-Za-z0-9]*)'
@_(r'"(?:[^"\\]|\\.)*"')
def STRING(self,t):
t.value = eval(t.value)
return t
@_(r'\n+')
def ignore_newline(self,t):
self.lineno+=len(t.value)
@_(r"[#][^#]+[#]")
def ignore_comment(self,t):
self.lineno+=t.value.count("\n")
NUMBER = r'\d+'
def NUMBER(self,t):
t.value = int(t.value)
return t
class DummyToken:
def __getattr__(self):
return None
import sys
def caller_id():
return sys._getframe(2).f_code.co_name
class Evaluator:
def __init__(self): pass
def run(self,prog):
if type(prog)!=list: prog = list(Lexer().tokenize(prog))
self.prog = prog
self.pos = 0
self.values = dict()
self.flag = False
while self.pos<len(self.prog):
tok = self.prog[self.pos]
self.pos+=1
if hasattr(self,"do_"+tok.type):
try:
getattr(self,"do_"+tok.type)(tok)
except AssertionError as e:
msg = e.args[0]
print("Error: "+msg)
return
else:
print(f"Error: invalid state with token {tok.type} ({tok.value!r}) at line {tok.lineno!s}")
return
def next(self,*types):
cmd = caller_id()[len("command_"):]
assert self.pos<len(self.prog), f"unexpected EOF after command {cmd}"
if not types: types = Lexer.tokens
tok = self.prog[self.pos]
assert tok.type in types, f"invalid argument type {tok.type} for command {cmd}"
self.pos+=1
return tok
def peek(self):
cmd = caller_id()[len("command_"):]
try:
return self.prog[self.pos]
except:
return DummyToken()
def do_TARGET(self,t):
# just needs to be here so TARGET tokens don't cause an error
return
def do_COMMAND(self,t):
if hasattr(self,"command_"+t.value):
getattr(self,"command_"+t.value)()
else:
print(f"Error: unimplemented command {t.value!r} at line {t.lineno!s}")
@property
def targets(self):
targets = {}
for i, t in enumerate(self.prog):
if t.type=="TARGET":
targets[t.value]=i
return targets
def expand_values(self,s):
for key, value in self.values.items():
s = s.replace("{{"+key+"}}",str(value))
return s
def command_goto(self):
target = self.next("STEM")
assert target.value in self.targets, "Invalid goto target {target.value} at line {target.lineno}!"
self.pos = self.targets[target.value]
def command_print(self):
val = self.next("STRING").value
val = self.expand_values(val)
print(val)
def command_choose(self):
choices = dict()
while self.peek().type=="STRING":
label = self.next("STRING")
target = self.next("STEM")
assert target.value in self.targets, "Invalid goto target {target.value} in choose statement on line {target.lineno}"
choices[label.value]=target.value
for i, label in enumerate(choices.keys(),1):
print(f"{i}.) {label}")
inp = input("? ").strip()
try:
inp = int(inp)-1
assert inp>=0 and inp<len(choices.keys())
self.pos = self.targets[choices[list(choices.keys())[inp]]]
except: pass
def command_input(self):
key = self.next("STEM").value
prompt = self.next("STRING").value
print(self.expand_values(prompt))
empty = "You must give a value!"
if self.peek().type=="STRING":
empty = self.expand_values(self.next("STRING").value)
val = input("? ").strip()
while not val:
print(empty)
val = input("? ").strip()
self.values[key]=val
def command_set(self):
key = self.next("STEM").value
val = self.next("STRING","NUMBER","BOOLEAN").value
self.values[key]=val
def command_testequals(self):
key = self.next("STEM").value
if key not in self.values:
self.flag=False
return
self.flag = self.values[key]==self.next("STRING","NUMBER","BOOLEAN").value
def command_check(self):
key = self.next("STEM").value
if key not in self.values:
self.flag=False
return
assert type(self.values[key])==bool, f"Cannot use check on non-boolean value {self.values[key]!r}!"
self.flag = self.values[key]
def command_beq(self):
target = self.next("STEM")
assert target.value in self.targets, f"Invalid branch target {target.value} at line {target.lineno}!"
if not self.flag: return
self.pos = self.targets[target.value]
def command_bne(self):
target = self.next("STEM")
assert target.value in self.targets, f"Invalid branch target {target.value} at line {target.lineno}!"
if self.flag: return
self.pos = self.targets[target.value]
if __name__=="__main__":
_, file = sys.argv
with open(file) as f:
script = f.read()
evaluator = Evaluator()
evaluator.run(script)

18
conditionals.chs Normal file
View File

@ -0,0 +1,18 @@
input text "Type `foo`." "I didn't say \"type nothing\", I said \"type `foo`\"."
#`testequals` tests if a variable is equal to a value.#
#In this case, we're testing if the variable `text` is equal to the string "foo".#
testequals text "foo"
#`beq` will jump to the given target if the comparison is true.#
beq yes
#`bne` will jump to the given target if the comparison is false.#
bne no
print "This will never execute."
no:
print "You couldn't even follow that simple direction. Good job."
goto end
yes:
print "Congratulations! You actually listened to me!"
end:

26
input_and_choices.chs Normal file
View File

@ -0,0 +1,26 @@
#Name prompt, to show off the `input` command. #
#`input` has an optional third argument, where it's a string to print if the user doesn't provide any input.#
nameprompt:
input name "What's your name?" "You need a name to continue. It doesn't even have to be yours!"
top:
print "Well, {{name}}, you find yourself at the top of the mountain."
print "How do you get down?"
#The `choose` command takes a string and stem for each choice.#
#The string is a label, describing the choice, while the stem is the goto target if that choice is chosen.#
choose "Cause an avalanche" avalanche "Hike down the mountain like a normal person" hike
#If the user doesn't input a valid choice, it falls through, allowing you to handle an invalid choice on your own.#
print "That's not a choice!"
goto top
avalanche:
print "As the avalanche begins, you very quickly realize that you have no real way of outrunning the avalanche."
print "You suffocate to death beneath the snow."
print "You failed."
goto end
hike:
print "You hike down the mountain like a normal person, and you make it to the bottom unscathed."
print "You succeeded."
end:

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
sly==0.4