diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index ebe5bf4..a3d2558 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -16,16 +16,18 @@ jobs:
with:
go-version: '1.22'
- name: Unit Tests
- run: go test -v ./... -coverprofile=coverage.txt -covermode=atomic
- - name: Code Coverage
- uses: actions/upload-artifact@v4
- if: startsWith(github.ref, 'refs/tags/')
- with:
- name: coverage
- path: coverage.txt
+ run: go test -v ./... -coverprofile=coverage.out -covermode=atomic
+ # - name: Generate Coverage HTML
+ # run: go tool cover -html=coverage.out -o coverage.html
+ # - name: Code Coverage
+ # uses: actions/upload-artifact@v4
+ # with:
+ # name: coverage
+ # path: coverage.html
build_cli:
runs-on: ubuntu-latest
+ if: startsWith(github.ref, 'refs/tags/')
needs: build
strategy:
matrix:
@@ -47,7 +49,6 @@ jobs:
- name: Artifact Upload
uses: actions/upload-artifact@v4
- if: startsWith(github.ref, 'refs/tags/')
with:
name: fabled-story-book-${{matrix.os}}-${{matrix.arch}}
path: bin/*
@@ -64,14 +65,35 @@ jobs:
run: GOOS=js GOARCH=wasm tinygo build -o ./web/test.wasm -target wasm -no-debug ./cmd/wasm/main.go
- name: Artifact Upload
uses: actions/upload-artifact@v4
- if: startsWith(github.ref, 'refs/tags/')
with:
name: wasm
path: web/test.wasm
+ publish_web:
+ runs-on: ubuntu-latest
+ needs: build_web
+ permissions:
+ contents: read
+ deployments: write
+ steps:
+ - uses: actions/checkout@v4
+ - name: Download Artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: wasm
+ path: web
+ - name: Publish Web
+ uses: cloudflare/pages-action@v1
+ with:
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ accountId: 95ffb963e9a7dc68ae10699ffec445b2
+ projectName: fabled-story-book
+ directory: ./web
+ gitHubToken: ${{ secrets.GITHUB_TOKEN }}
+
publish_release:
runs-on: ubuntu-latest
- needs: [build_cli, build_web]
+ needs: [build_cli, publish_web]
permissions:
contents: write
if: startsWith(github.ref, 'refs/tags/')
@@ -88,5 +110,5 @@ jobs:
files: |
./dist/*
generate_release_notes: true
- name: Release Web ${{ github.ref_name }}
+ name: Fabled Story Book ${{ github.ref_name }}
target_commitish: ${{ github.sha }}
\ No newline at end of file
diff --git a/assets/example01/entrypoint.jabl b/assets/example01/entrypoint.jabl
index 2cf0dd1..cfdf8e5 100644
--- a/assets/example01/entrypoint.jabl
+++ b/assets/example01/entrypoint.jabl
@@ -6,13 +6,7 @@
print("You must escape ...")
choice("Look at skeleton", {
- if (get("examined-skeleton") == -100.6) {
- print("You have already examined this skeleton")
- } else {
- set("examined-skeleton", -100.6)
- goto("entrypoint.jabl")
- print("You find nothing of value")
- }
+ goto("skeleton.jabl")
})
choice("Go upstairs", {
goto("upstairs.jabl")
diff --git a/assets/example01/hallway.jabl b/assets/example01/hallway.jabl
index 2c78e56..09ae4e5 100644
--- a/assets/example01/hallway.jabl
+++ b/assets/example01/hallway.jabl
@@ -4,10 +4,14 @@
if (get("has-oak-door-key") == 1) {
goto("ending.jabl")
} else {
- print("It's locked")
+ print("It's locked, you need a key.")
+
+ choice("Back to the dining hall", {
+ goto("upstairs.jabl")
+ })
}
})
choice("Back to the dining hall", {
- goto("hallway.jabl")
+ goto("upstairs.jabl")
})
}
\ No newline at end of file
diff --git a/assets/example01/shabby-door.jabl b/assets/example01/shabby-door.jabl
index be24487..8c68795 100644
--- a/assets/example01/shabby-door.jabl
+++ b/assets/example01/shabby-door.jabl
@@ -4,20 +4,41 @@
if (get("has-oak-door-key") == 1) {
print("The mangled remains of a frog are lying on the floor.")
- choice("")
+ choice("Investigate the frog", {
+ print("You find nothing of value.")
+ choice("Ok", {goto("shabby-door.jabl")})
+ })
} else {
- print("An enormous frog is staring at you from the other side of the room.")
- choice("Attack the frog", {
- fight("frog-1", {
- goto("guard-room/success.jabl")
- }, {
- goto("guard-room/failure.jabl")
+ if (get("attacked-frog") != 1) {
+ print("An enormous frog is staring at you from the other side of the room.")
+ set("frog-health", 10d6)
+ } else {
+ if (get("frog-health") > 0) {
+ print("The frog glares at you but makes no move to attack back.")
+ }
+ }
+
+ if (get("frog-health") <= 0) {
+ print("The frog is dead.")
+ choice("Search the frog", {
+ print("You find a key.")
+ set("has-oak-door-key", 1)
+ choice("Ok", {goto("shabby-door.jabl")})
})
- })
+ } else {
+ print("The frog looks like it is about to attack you.")
+
+ choice("Attack the frog", {
+ print("You attack the frog, it is not yet dead")
+ set("frog-health", get("frog-health") - 1d6)
+ set("attacked-frog", 1)
+ choice("Ok", {goto("shabby-door.jabl")})
+ })
+ }
}
choice("Leave", {
- goto("")
+ goto("upstairs.jabl")
})
}
\ No newline at end of file
diff --git a/assets/example01/skeleton.jabl b/assets/example01/skeleton.jabl
new file mode 100644
index 0000000..d88bc3f
--- /dev/null
+++ b/assets/example01/skeleton.jabl
@@ -0,0 +1,10 @@
+{
+ if (get("examined-skeleton") == 1) {
+ print("You have already examined this skeleton")
+ } else {
+ set("examined-skeleton", 1)
+ print("You find nothing of value")
+ }
+
+ choice("Continue", { goto("entrypoint.jabl") })
+}
\ No newline at end of file
diff --git a/assets/example01/upstairs.jabl b/assets/example01/upstairs.jabl
index 4e57273..ea18e3c 100644
--- a/assets/example01/upstairs.jabl
+++ b/assets/example01/upstairs.jabl
@@ -1,12 +1,20 @@
{
- print("You are standing a grand dining room. A long table made up with a tasty looking feast stands in the centre of the room")
- print("The food is still steaming hot and looks delicious. You are feeling very hungry")
+ if (get("food-eaten") != 1) {
+ print("You are standing a grand dining room. A long table made up with a tasty looking feast stands in the centre of the room")
+ print("The food is still steaming hot and looks delicious. You are feeling very hungry")
+ } else {
+ print("You are standing a grand dining room. A long table now lays bare in the centre of the room")
+ }
print("On the left of the hall is a shabby looking door")
print("The right opens into a grand entrance hallway")
- choice("Eat the food", {
- print("You eat the food")
- })
+ if (get("food-eaten") != 1) {
+ choice("Eat the food", {
+ set("food-eaten", 1)
+ print("You eat the food")
+ choice("Ok", {goto("upstairs.jabl")})
+ })
+ }
choice("Exit through shabby door", {
goto("shabby-door.jabl")
})
diff --git a/cmd/wasm/main.go b/cmd/wasm/main.go
index 1dcd4d0..47bcbdb 100644
--- a/cmd/wasm/main.go
+++ b/cmd/wasm/main.go
@@ -72,8 +72,24 @@ func execSection(this js.Value, inputs []js.Value) any {
return nil
}
+func evalCode(this js.Value, inputs []js.Value) any {
+ callback := inputs[1]
+
+ // The interpreter delegates back to the loader for getting the code to execute from an identifier
+ interpreter.Evaluate(inputs[0].String(), state, func(section *jabl.Result, err error) {
+ jsonValueOfRes, err := json.Marshal(section)
+ if err != nil {
+ callback.Invoke(js.Null(), err.Error())
+ } else {
+ callback.Invoke(string(jsonValueOfRes), js.Null())
+ }
+ })
+ return nil
+}
+
func registerCallbacks() {
js.Global().Set("execSection", js.FuncOf(execSection))
+ js.Global().Set("evalCode", js.FuncOf(evalCode))
}
// A state mapper that delegates back to Javascript for getting and setting state
diff --git a/internal/cli/cli.go b/internal/cli/cli.go
index 89dfd6f..2d888b0 100644
--- a/internal/cli/cli.go
+++ b/internal/cli/cli.go
@@ -74,6 +74,13 @@ func (p *jablProgram) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
p.transition = ""
}
p.err = msg.err
+
+ if p.transition != "" {
+ p.currentSelection = 0
+ p.currentSection = p.transition
+ return p, execSection(p.interpreter, p.state, p.transition)
+ }
+
return p, nil
}
@@ -82,6 +89,7 @@ func (p *jablProgram) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (p *jablProgram) View() string {
+
// The header
s := p.text + "\n\n"
diff --git a/internal/cli/state.go b/internal/cli/state.go
index 7743a95..07216c7 100644
--- a/internal/cli/state.go
+++ b/internal/cli/state.go
@@ -1,19 +1,20 @@
package cli
-import "fmt"
-
-type stateMapper struct{}
+type stateMapper struct {
+ cache map[string]float64
+}
func NewStateMapper() *stateMapper {
- return &stateMapper{}
+ return &stateMapper{
+ cache: map[string]float64{},
+ }
}
func (s *stateMapper) Get(key string) (float64, error) {
- fmt.Println("Get", key)
- return 0, nil
+ return s.cache[key], nil
}
func (s *stateMapper) Set(key string, value float64) error {
- fmt.Println("Set", key, value)
+ s.cache[key] = value
return nil
}
diff --git a/web/app.css b/web/app.css
new file mode 100644
index 0000000..902e513
--- /dev/null
+++ b/web/app.css
@@ -0,0 +1,70 @@
+body {
+ margin: 0;
+ padding: 0;
+ min-height: 100%;
+ min-height: --webkit-fill-available;
+
+ overflow: none;
+ touch-action: pinch-zoom;
+
+ background-color: #01130d;
+ font-family: "Courier New", Courier, monospace;
+}
+
+.container {
+ height: 88vh;
+ max-width: 600px;
+ margin: auto;
+ display: flex;
+ flex-direction: column;
+ overflow: none;
+ padding: 1em;
+}
+
+.console {
+ display: flex;
+ flex: 1;
+ padding: 2em;
+ background-color: #022117;
+ color: #41ff00;
+ border-radius: 18px;
+ margin-bottom: 0;
+ overflow-y: scroll;
+}
+
+.console-text {
+ animation: type 2s steps(40) infinite;
+}
+
+.buttons {
+ margin-top: 1em;
+ display: flex;
+ justify-content: start;
+ gap: 1em;
+ width: 100%;
+ overflow-x: scroll;
+}
+
+.button {
+ background-color: #022117;
+ border-radius: 5px;
+ border: none;
+ color: #41ff00;
+ cursor: pointer;
+ fill: #41ff00;
+ font-family: "Courier New", Courier, monospace;
+ font-size: 1em;
+ text-align: left;
+ padding: 1em;
+ min-width: 200px;
+}
+
+.button:hover {
+ background-color: #023021;
+}
+
+.icon {
+ color: #41ff00;
+ width: 16px;
+ height: 16px;
+}
diff --git a/web/app.js b/web/app.js
new file mode 100644
index 0000000..7996a94
--- /dev/null
+++ b/web/app.js
@@ -0,0 +1,138 @@
+let textIndex = 0;
+let intervalId = null;
+
+const renderText = (text) => {
+ const consoleText = document.querySelector(".console-text");
+ if (intervalId) {
+ consoleText.innerHTML = "";
+ clearInterval(intervalId);
+ textIndex = 0;
+ }
+
+ intervalId = setInterval(() => {
+ if (text && textIndex < text.length) {
+ if (text.charAt(textIndex) === "\n") {
+ consoleText.innerHTML += "
";
+ } else {
+ consoleText.innerHTML += text.charAt(textIndex);
+ }
+ // scroll consoleText to the bottom of what it is displaying
+ consoleText.parentElement.scrollTop =
+ consoleText.parentElement.scrollHeight;
+ textIndex++;
+ } else {
+ clearInterval(intervalId);
+ }
+ }, 10);
+};
+
+const renderChoices = (choices) => {
+ const choiceButtons = document.querySelector(".buttons");
+ choiceButtons.innerHTML = "";
+ (choices || []).forEach((choice) => {
+ const button = document.createElement("button");
+ button.classList.add("button");
+
+ button.innerHTML = choice.text;
+ button.addEventListener("click", onChoiceSelected.bind(null, choice.code));
+ choiceButtons.appendChild(button);
+ });
+};
+
+const onChoiceSelected = (code) => {
+ jablEval(code)
+ .then((result) => {
+ render(result, null);
+ })
+ .catch((e) => {
+ render(null, e);
+ });
+};
+
+const render = (result, err) => {
+ if (err) {
+ renderText(err);
+ return;
+ }
+
+ renderText(result.output);
+ renderChoices(result.choices);
+ if (result && result.transition && result.transition.length > 0) {
+ const next = exec(result.transition)
+ .then((a) => {
+ render(a, null);
+ })
+ .catch((e) => {
+ render(null, e);
+ });
+ }
+};
+
+const run = async () => {
+ if (!WebAssembly.instantiateStreaming) {
+ WebAssembly.instantiateStreaming = async (resp, importObject) => {
+ const source = await (await resp).arrayBuffer();
+ return await WebAssembly.instantiate(source, importObject);
+ };
+ }
+
+ const go = new Go();
+ const { instance } = await WebAssembly.instantiateStreaming(
+ fetch("test.wasm"),
+ go.importObject
+ );
+ go.run(instance);
+
+ window.loadSection = (identifier, callback) => {
+ console.log("loading section " + identifier);
+ fetch(
+ "https://raw.githubusercontent.com/jasoncabot/fabled-story-book/main/assets/example01/" +
+ identifier
+ )
+ .then((response) => response.text())
+ .then((text) => {
+ callback(text, null);
+ })
+ .catch((err) => {
+ callback(null, err);
+ });
+ };
+
+ exec("entrypoint.jabl")
+ .then((result) => {
+ render(result, null);
+ })
+ .catch((e) => {
+ render(null, e);
+ });
+};
+
+const exec = (sectionId) => {
+ return new Promise((resolve, reject) => {
+ console.log("executing section " + sectionId);
+ window.execSection(sectionId, (value, err) => {
+ if (err) {
+ reject(err);
+ } else {
+ try {
+ result = JSON.parse(value);
+ resolve(result);
+ } catch (e) {
+ reject(e);
+ }
+ }
+ });
+ });
+};
+
+const jablEval = (code) => {
+ return new Promise((resolve, reject) =>
+ window.evalCode(code, (value, err) => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(JSON.parse(value));
+ }
+ })
+ );
+};
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..8c7fca9
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,32 @@
+
+
+
- -
- -