~nromdotcom/gemif

1bf434089d4531dcf6ce4735f644d52949571b1c — Norm MacLennan 1 year, 2 days ago 4cd93f0
First pass at conditionals
M pkg/gamemanager/gamemanager.go => pkg/gamemanager/gamemanager.go +25 -3
@@ 9,8 9,9 @@ import (

// GameState contains state information about an active game in progress.
type GameState struct {
	StoryID     string `json:"si"`
	CurrentRoom string `json:"cr"`
	StoryID     string   `json:"si"`
	CurrentRoom string   `json:"cr"`
	Conditions  []string `json:"cn"`
}

// GameManager contains configuration info about stories and managed GameState objects.


@@ 46,6 47,7 @@ func (gm *GameManager) StartGame(storyID string) (GameState, error) {
	return GameState{
		StoryID:     storyID,
		CurrentRoom: gm.config.Stories[storyID].Rooms[0].ID,
		Conditions:  []string{},
	}, nil
}



@@ 83,8 85,15 @@ func (gm *GameManager) UseExit(gameState GameState, exitID string) (GameState, e

	for i := range currentRoom.Exits {
		if currentRoom.Exits[i].ID == exitID {
			gameState.CurrentRoom = currentRoom.Exits[i].Destination
			if currentRoom.Exits[i].IfCondition != "" && !isInSlice(gameState.Conditions, currentRoom.Exits[i].IfCondition) {
				fmt.Printf("Cant use exit! %s\n", currentRoom.Exits[i].ID)
				continue
			}

			gameState.CurrentRoom = currentRoom.Exits[i].Destination
			if currentRoom.Exits[i].SetCondition != "" {
				gameState.Conditions = append(gameState.Conditions, currentRoom.Exits[i].SetCondition)
			}
			return gameState, nil
		}
	}


@@ 122,6 131,10 @@ func (gm *GameManager) ConstructSpeculativeStates(gameState GameState, currentRo
	speculativeStates := make([]SpeculativeState, len(currentRoom.Exits))

	for i, e := range currentRoom.Exits {
		if e.IfCondition != "" && !isInSlice(gameState.Conditions, e.IfCondition) {
			continue
		}

		newState, exitErr := gm.UseExit(gameState, e.ID)
		if exitErr != nil {
			return speculativeStates, fmt.Errorf("trouble using exit %s: %w", e.ID, exitErr)


@@ 161,3 174,12 @@ func (gm *GameManager) ConstructStartingState(stories []StoryMetadata) ([]Specul

	return speculativeStates, nil
}

func isInSlice(slice []string, val string) bool {
	for _, item := range slice {
		if item == val {
			return true
		}
	}
	return false
}

M pkg/gamemanager/room.go => pkg/gamemanager/room.go +5 -3
@@ 10,7 10,9 @@ type Room struct {

// Exit describes actions applied to GameState as part of leaving a Room.
type Exit struct {
	ID          string `yaml:"exit_id" json:"exit_id" binding:"uuid"`
	Description string `yaml:"exit_description" json:"exit_description"`
	Destination string `yaml:"destination_id" json:"destination_id" binding:"uuid"`
	ID           string `yaml:"exit_id" json:"exit_id" binding:"uuid"`
	Description  string `yaml:"exit_description" json:"exit_description"`
	Destination  string `yaml:"destination_id" json:"destination_id" binding:"uuid"`
	SetCondition string `yaml:"set_condition" json:"set_condition"`
	IfCondition  string `yaml:"if_condition" json:"if_condition"`
}

A pkg/scenerenderer/scenerenderer.go => pkg/scenerenderer/scenerenderer.go +41 -0
@@ 0,0 1,41 @@
package scenerenderer

import (
	"bytes"
	"fmt"
	"io"
	"text/template"

	"github.com/pitr/gig"
)

// Template contains the room templates.
type Template struct {
	templates *template.Template
}

func New() Template {
	return Template{
		template.New(""),
	}
}

// Render is a simple rendering function for our text/template to Gemini for gig.
func (t *Template) render(w io.Writer, name string, data interface{}, c gig.Context) error {
	return t.templates.ExecuteTemplate(w, name, data)
}

func (t *Template) ExecuteTemplate(storyID string, roomID string, roomDesc string, data interface{}) (string, error) {
	templateName := fmt.Sprintf("%s%s", storyID, roomID)
	roomTmpl := t.templates.Lookup(templateName)
	if roomTmpl == nil {
		template.Must(t.templates.New(templateName).Parse(roomDesc))
	}

	var compiledDesc bytes.Buffer
	if compileErr := t.templates.ExecuteTemplate(&compiledDesc, templateName, data); compileErr != nil {
		return "", fmt.Errorf("could not construct room description: %w", compileErr)
	}

	return compiledDesc.String(), nil
}

M pkg/web/router.go => pkg/web/router.go +4 -1
@@ 3,6 3,7 @@ package web
import (
	"fmt"
	"gemif/pkg/gamemanager"
	"gemif/pkg/scenerenderer"
	"os"

	"github.com/markbates/pkger"


@@ 21,6 22,8 @@ func StartRouter(gm *gamemanager.GameManager) {
		os.Exit(1)
	}

	sceneRenderer := scenerenderer.New()

	g := gig.Default()
	g.HideBanner = true
	g.Renderer = &Template{


@@ 28,7 31,7 @@ func StartRouter(gm *gamemanager.GameManager) {
	}

	g.Handle("/", handleHome(gm))
	g.Handle("/game/:statetoken", handleGame(gm))
	g.Handle("/game/:statetoken", handleGame(gm, sceneRenderer))
	g.Handle("/docs*", handleStatic)

	panic(g.Run("my.crt", "my.key"))

M pkg/web/routes.go => pkg/web/routes.go +11 -3
@@ 3,6 3,7 @@ package web
import (
	"fmt"
	"gemif/pkg/gamemanager"
	"gemif/pkg/scenerenderer"
	"io/ioutil"
	"net/url"
	"path"


@@ 22,7 23,7 @@ func handleStatic(c gig.Context) error {
		p = "/index.gmi"
	}

	name := filepath.Join("/static/docs", path.Clean("/"+p)) // "/"+ for security
	name := filepath.Join("/static/docs", path.Clean("/"+p))

	f, fileErr := pkger.Open(name)
	if fileErr != nil {


@@ 53,7 54,7 @@ func handleHome(gm *gamemanager.GameManager) func(gig.Context) error {
	}
}

func handleGame(gm *gamemanager.GameManager) func(gig.Context) error {
func handleGame(gm *gamemanager.GameManager, renderer scenerenderer.Template) func(gig.Context) error {
	return func(c gig.Context) error {
		gameState, gameErr := gm.DeserializeState(c.Param("statetoken"))
		if gameErr != nil {


@@ 65,6 66,13 @@ func handleGame(gm *gamemanager.GameManager) func(gig.Context) error {
			return fmt.Errorf("could not find room: %w", roomErr)
		}

		renderedRoom, renderErr := renderer.ExecuteTemplate(gameState.StoryID, currentRoom.ID, currentRoom.Description, map[string]interface{}{
			"Conditions": gameState.Conditions,
		})
		if renderErr != nil {
			return fmt.Errorf("could not render room %s", renderErr)
		}

		speculativeStates, speculationErr := gm.ConstructSpeculativeStates(gameState, currentRoom)
		if speculationErr != nil {
			return fmt.Errorf("unable to render exits: %w", speculationErr)


@@ 72,7 80,7 @@ func handleGame(gm *gamemanager.GameManager) func(gig.Context) error {

		return c.Render("gemif:/static/templates/room.gmi.tmpl", map[string]interface{}{
			"RoomName":    currentRoom.Name,
			"Description": currentRoom.Description,
			"Description": renderedRoom,
			"Actions":     speculativeStates,
		})
	}

M static/templates/room.gmi.tmpl => static/templates/room.gmi.tmpl +2 -0
@@ 5,5 5,7 @@
{{.Description}}

{{- range $key, $value := .Actions}}
  {{- if ne $value.StateToken ""}}
=> /game/{{$value.StateToken}} {{$value.Description}}
  {{- end}}
{{- end}}
\ No newline at end of file

M stories/sample-story.yml => stories/sample-story.yml +28 -1
@@ 24,12 24,15 @@ rooms:
  - exit_description: I want to make choice a
    destination_id: made_choice_a
    exit_id: make_choice_a
    set_condition: choice_a
  - exit_description: I want to make choice b
    destination_id: made_choice_b
    exit_id: make_choice_b
    set_condition: choice_b
  - exit_description: If you choose not to decide you still have made a choice
    destination_id: made_no_choice
    exit_id: make_no_choice
    set_condition: no_choice
- room_id: made_choice_a
  room_name: Doing Choice A
  room_description: >


@@ 62,5 65,29 @@ rooms:
    exit_description: Or maybe choice is an illusion
- room_id: the_end
  room_name: The End
  room_description: |
    Though because this is a simple demo, you always end up in the same place.

    {{ range $value := .Conditions}}
      {{- if eq $value "choice_a"}}
    oh cool, you made choice a! nice work.
      {{- end}}
      {{- if eq $value "choice_b"}}
    oh you made choice b, that's too bad :(
      {{- end}}
      {{- if eq $value "no_choice"}}
    wow you're soooo cool and mysterious, making no choice.
    you really thing that makes you interesting? well it doesn't.
      {{- end}}
    {{- end}}
  exits:
  - exit_id: bonus_round
    destination_id: secret_level
    if_condition: no_choice
    exit_description: oh, what's this?
- room_id: secret_level
  room_name: Bonus Round!
  room_description: >
    Though because this is a simple demo, you always end up in the same place.
\ No newline at end of file
    We're all so proud of you for being so cool and awesome that you didn't make a choice.

    Nobody else got this ending!
\ No newline at end of file