Skip to content

Commit 03da1ea

Browse files
committed
Initial commit. Basic working version.
0 parents  commit 03da1ea

13 files changed

+959
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# examples/*.yarnc
2+
# examples/*.csv
3+
**/__pycache__
4+
.python-version

README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# YarnRunner-Python
2+
3+
An **unofficial** Python interpreter for compiled [Yarn Spinner](https://yarnspinner.dev/) programs. _Documentation incomplete._
4+
5+
#### Using the library
6+
7+
Here's an example illustrating how to use the library:
8+
9+
```py
10+
from yarnrunner_python import YarnRunner
11+
12+
# Open the compiled story and strings CSV.
13+
story_f = open('story.yarnc', 'rb')
14+
strings_f = open('story.csv', 'r')
15+
16+
# Create the runner
17+
runner = YarnRunner(story_f, strings_f, autostart=False)
18+
19+
# Register any command handlers
20+
# (see https://yarnspinner.dev/docs/writing/nodes-and-content/#commands)
21+
def custom_command(arg1, arg2):
22+
pass
23+
24+
runner.add_command_handler("customCommand", custom_command)
25+
26+
# Start the runner and run until you hit a choice point
27+
runner.resume()
28+
29+
# Access the lines printed from the story
30+
print('\n'.join(runner.get_lines()))
31+
32+
# Access the choices
33+
for choice in runner.get_choices():
34+
print(f"[{choice.index}] ${choice.text}")
35+
36+
# Make a choice and run until the next choice point or the end
37+
runner.choose(0)
38+
39+
# Access the new lines printed from the last run
40+
print('\n'.join(runner.get_lines()))
41+
42+
# Are we done?
43+
if runner.finished:
44+
print("Woohoo! Our story is over!")
45+
```
46+
47+
A few gotchas to look out for:
48+
49+
- Calling `runner.get_lines()` or `runner.get_line()` is a destructive operation, it fetches the current lines (or line) from the line buffer and then pops them from the buffer. Therefore, calling `runner.get_lines()` twice in a row without making a choice will give you different results. **Feedback on this approach is welcome!**
50+
- Make sure to open the compiled story file as a binary file (see the above example, use `open(filename, 'rb')`) in order for it to be properly parsed by the compiled protobuf library.
51+
- Unless you pass `autostart=False` to the runner when creating it, it will automatically start and run to the next choice point.
52+
53+
Only a subset of Yarn Spinner opcodes are currently implemented. This will certainly change over time. The current status is:
54+
55+
| OpCode | Status |
56+
| ---------------- | ---------------------------------------------------- |
57+
| `JUMP_TO` | 🚫  Not Implemented |
58+
| `JUMP` | 🚫  Not Implemented |
59+
| `RUN_LINE` |  Implemented in `runner.__run_line` |
60+
| `RUN_COMMAND` |  Implemented in `runner.__run_command` |
61+
| `ADD_OPTION` |  Implemented in `runner.__add_option` |
62+
| `SHOW_OPTIONS` |  Implemented in `runner.__show_options` |
63+
| `PUSH_STRING` | 🚫  Not Implemented |
64+
| `PUSH_FLOAT` | 🚫  Not Implemented |
65+
| `PUSH_BOOL` | 🚫  Not Implemented |
66+
| `PUSH_NULL` | 🚫  Not Implemented |
67+
| `JUMP_IF_FALSE` | 🚫  Not Implemented |
68+
| `POP` | 🚫  Not Implemented |
69+
| `CALL_FUNC` | 🚫  Not Implemented |
70+
| `PUSH_VARIABLE` | 🚫  Not Implemented |
71+
| `STORE_VARIABLE` | 🚫  Not Implemented |
72+
| `STOP` |  Implemented in `runner.stop` |
73+
| `RUN_NODE` |  Implemented in `runner.__run_node` |
74+
75+
## Development
76+
77+
This project uses Python 3 and has a basic test suite.
78+
79+
1. `pip install -r requirements.txt`
80+
2. `py.test`
81+
82+
## Updating the examples
83+
84+
The source code of the examples are located inside `*.yarn` files. `*.csv` and `*.yarnc` files are generated via the Yarn Spinner compiler. To compile these files, follow the below steps:
85+
86+
1. Install the [Yarn Spinner Console](https://github.com/YarnSpinnerTool/YarnSpinner-Console) program `ysc`. Basic binaries are available on [their releases page](https://github.com/YarnSpinnerTool/YarnSpinner-Console/releases).
87+
2. From the `examples/` directory, run `ysc compile [filename].yarn`. For example, to compile the basic example used in `tests/test_basic.py`, use `ysc compile basic.yarn`.
88+
- This will output `*.csv` and `*.yarnc` files in the current directory, overwriting any files already present with the same name.
89+
90+
Currently `*.csv` and `*.yarnc` files are committed to version control to make it easier to run our test suite. They will likely be gitignored later once we have a better build process for these files.

examples/basic.csv

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
id,text,file,node,lineNumber
2+
basic-Start-0,This is the first node.,basic,Start,3
3+
basic-Start-1,Choice 1,basic,Start,5
4+
basic-Start-2,Choice 2,basic,Start,6
5+
basic-choice_1-3,"Here is the node visited as a **result** of the __first__ ""choice"", with a comma.",basic,choice_1,11

examples/basic.yarn

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
title: Start
2+
---
3+
This is the first node.
4+
5+
[[Choice 1|choice_1]]
6+
[[Choice 2|choice_2]]
7+
===
8+
title: choice_1
9+
---
10+
11+
Here is the node visited as a **result** of the __first__ "choice", with a comma.
12+
13+
<<runACommand arg1 2 event:/event/event_name >>
14+
15+
===

examples/basic.yarnc

264 Bytes
Binary file not shown.

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
protobuf==3.19.1
2+
pytest==6.2.5

tests/__init__.py

Whitespace-only changes.

tests/context.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import os
2+
import sys
3+
sys.path.insert(0, os.path.abspath(
4+
os.path.join(os.path.dirname(__file__), '..')))
5+
6+
# the "nopep8" is to ensure this is below "sys.path", autopep8 will autoformat it above otherwise
7+
from yarnrunner_python import YarnRunner # nopep8
8+
9+
# for the structure of this file, see https://docs.python-guide.org/writing/structure/#test-suite

tests/test_basic.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import os
2+
from .context import YarnRunner
3+
4+
compiled_yarn_f = open(os.path.join(os.path.dirname(
5+
__file__), '../examples/basic.yarnc'), 'rb')
6+
names_csv_f = open(os.path.join(os.path.dirname(
7+
__file__), '../examples/basic.csv'), 'r')
8+
9+
runner = YarnRunner(compiled_yarn_f, names_csv_f)
10+
11+
12+
def test_start_node_text():
13+
assert "This is the first node." == runner.get_line()
14+
assert not runner.has_line()
15+
assert not runner.finished
16+
17+
18+
def test_start_node_choices():
19+
choices = runner.get_choices()
20+
21+
assert len(choices) == 2
22+
assert choices[0]["choice"] == "choice_1"
23+
assert choices[1]["choice"] == "choice_2"
24+
assert choices[0]["text"] == "Choice 1"
25+
assert choices[1]["text"] == "Choice 2"
26+
27+
28+
side_effect = None
29+
30+
31+
def run_a_command(arg1, arg2, arg3):
32+
global side_effect
33+
side_effect = arg3
34+
35+
36+
runner.add_command_handler("runACommand", run_a_command)
37+
38+
39+
def test_start_node_choose():
40+
runner.choose(0)
41+
42+
assert "Here is the node visited as a **result** of the __first__ \"choice\", with a comma." == runner.get_line()
43+
assert not runner.has_line()
44+
assert runner.finished
45+
46+
# ensure the command has run
47+
assert side_effect == "event:/event/event_name"

yarn_spinner.proto

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
syntax = "proto3";
2+
package Yarn;
3+
4+
// Copyright (c) 2015-2017 Secret Lab Pty. Ltd. and Yarn Spinner contributors.
5+
// This file is copied from YarnSpinner/YarnSpinner@fb2d6db, which is licensed MIT.
6+
// It should be updated if Yarn makes any changes to their protocol for compiled code.
7+
8+
// A complete Yarn program.
9+
message Program {
10+
11+
// The name of the program.
12+
string name = 1;
13+
14+
// The collection of nodes in this program.
15+
map<string, Node> nodes = 2;
16+
}
17+
18+
// A collection of instructions
19+
message Node {
20+
// The name of this node.
21+
string name = 1;
22+
23+
// The list of instructions in this node.
24+
repeated Instruction instructions = 2;
25+
26+
// A jump table, mapping the names of labels to positions in the
27+
// instructions list.
28+
map<string, int32> labels = 3;
29+
30+
// The tags associated with this node.
31+
repeated string tags = 4;
32+
33+
// the entry in the program's string table that contains the original
34+
// text of this node; null if this is not available
35+
string sourceTextStringID = 5;
36+
}
37+
38+
// A single Yarn instruction.
39+
message Instruction {
40+
41+
// The operation that this instruction will perform.
42+
OpCode opcode = 1;
43+
44+
// The list of operands, if any, that this instruction uses.
45+
repeated Operand operands = 2;
46+
47+
// The type of instruction that this is.
48+
enum OpCode {
49+
50+
// Jumps to a named position in the node.
51+
// opA = string: label name
52+
JUMP_TO = 0;
53+
54+
// Peeks a string from stack, and jumps to that named position in
55+
// the node.
56+
// No operands.
57+
JUMP = 1;
58+
59+
// Delivers a string ID to the client.
60+
// opA = string: string ID
61+
RUN_LINE = 2;
62+
63+
// Delivers a command to the client.
64+
// opA = string: command text
65+
RUN_COMMAND = 3;
66+
67+
// Adds an entry to the option list (see ShowOptions).
68+
// opA = string: string ID for option to add
69+
ADD_OPTION = 4;
70+
71+
// Presents the current list of options to the client, then clears
72+
// the list. The most recently selected option will be on the top
73+
// of the stack when execution resumes.
74+
// No operands.
75+
SHOW_OPTIONS = 5;
76+
77+
// Pushes a string onto the stack.
78+
// opA = string: the string to push to the stack.
79+
PUSH_STRING = 6;
80+
81+
// Pushes a floating point number onto the stack.
82+
// opA = float: number to push to stack
83+
PUSH_FLOAT = 7;
84+
85+
// Pushes a boolean onto the stack.
86+
// opA = bool: the bool to push to stack
87+
PUSH_BOOL = 8;
88+
89+
// Pushes a null value onto the stack.
90+
// No operands.
91+
PUSH_NULL = 9;
92+
93+
// Jumps to the named position in the the node, if the top of the
94+
// stack is not null, zero or false.
95+
// opA = string: label name
96+
JUMP_IF_FALSE = 10;
97+
98+
// Discards top of stack.
99+
// No operands.
100+
POP = 11;
101+
102+
// Calls a function in the client. Pops as many arguments as the
103+
// client indicates the function receives, and the result (if any)
104+
// is pushed to the stack.
105+
106+
// opA = string: name of the function
107+
CALL_FUNC = 12;
108+
109+
// Pushes the contents of a variable onto the stack.
110+
// opA = name of variable
111+
PUSH_VARIABLE = 13;
112+
113+
// Stores the contents of the top of the stack in the named
114+
// variable.
115+
// opA = name of variable
116+
STORE_VARIABLE = 14;
117+
118+
// Stops execution of the program.
119+
// No operands.
120+
STOP = 15;
121+
122+
// Run the node whose name is at the top of the stack.
123+
// No operands.
124+
RUN_NODE = 16;
125+
}
126+
}
127+
128+
// A value used by an Instruction.
129+
message Operand {
130+
131+
// The type of operand this is.
132+
oneof value {
133+
134+
// A string.
135+
string string_value = 1;
136+
137+
// A boolean (true or false).
138+
bool bool_value = 2;
139+
140+
// A floating point number.
141+
float float_value = 3;
142+
}
143+
}

yarnrunner_python/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .runner import YarnRunner

0 commit comments

Comments
 (0)