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 @@ + + + + + + + + + + + + + + Fabled Story Book + + + +
+
+ +
+
+
+ + diff --git a/web/wasm_exec.html b/web/wasm_exec.html deleted file mode 100644 index d444acd..0000000 --- a/web/wasm_exec.html +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - Go wasm - - - - - - - -

JABL

- -

Base URL

- - -

Input

- - -

Output

- - -

- -

- -