~nromdotcom/gemif

2a3f890ddab872d9b4e2ba4b1a2f413ad934ad3d — Norm MacLennan 11 months ago ac0f0be
Drastically simplify .gemif story format
3 files changed, 128 insertions(+), 25 deletions(-)

M cmd/gemifc/main.go
A cmd/gemifc/roomparser.go
M pkg/gamemanager/room.go
M cmd/gemifc/main.go => cmd/gemifc/main.go +16 -18
@@ 9,7 9,7 @@ import (
	"log"
	"os"
	"path/filepath"
	"strings"
	"regexp"

	"gopkg.in/yaml.v2"
)


@@ 51,6 51,7 @@ func findFilesWithExtension(root, pattern string) ([]string, error) {
}

func processFile(path string) ([]gamemanager.Room, error) {
	roomParser := newRoomParser()
	fileRooms := []gamemanager.Room{}

	thisFile, err := readFile(path)


@@ 58,28 59,25 @@ func processFile(path string) ([]gamemanager.Room, error) {
		return fileRooms, fmt.Errorf("couldn't read .gemif file: %w", err)
	}

	isMetadata := false

	currentRoom := gamemanager.Room{}

	for _, filePiece := range strings.Split(thisFile, "---") {
		if filePiece == "" {
			isMetadata = !isMetadata

	sceneSeparator := regexp.MustCompile("(?m)^---")
	for _, room := range sceneSeparator.Split(thisFile, -1) {
		if room == "" {
			continue
		}

		if isMetadata {
			if err := yaml.Unmarshal([]byte(filePiece), &currentRoom); err != nil {
				return fileRooms, fmt.Errorf("couldn't parse story metadata from yaml: %w", err)
			}
		} else {
			currentRoom.Description = filePiece
			fileRooms = append(fileRooms, currentRoom)
			currentRoom = gamemanager.Room{}
		currentRoom, err := roomParser.findRoomInfo(room)
		if err != nil {
			log.Fatal(err)
		}

		currentRoom.Exits, err = roomParser.processExits(room)
		if err != nil {
			log.Fatal(err)
		}

		isMetadata = !isMetadata
		currentRoom.Description = roomParser.getRoomDescription(room)
		fileRooms = append(fileRooms, currentRoom)
		currentRoom = gamemanager.Room{}
	}

	return fileRooms, nil

A cmd/gemifc/roomparser.go => cmd/gemifc/roomparser.go +112 -0
@@ 0,0 1,112 @@
package main

import (
	"errors"
	"fmt"
	"gemif/pkg/gamemanager"
	"regexp"
	"strings"
)

// ErrInvalidRoomHeader happens when the header of a room is malformed or missing.
var ErrInvalidRoomHeader = errors.New("invalid or missing room header")

// ErrInvalidExitDescription happens when an exit is malformed.
var ErrInvalidExitDescription = errors.New("invalid exit description")

type roomParser struct {
	roomInfoRexp  *regexp.Regexp
	exitRexp      *regexp.Regexp
	ifCondRexp    *regexp.Regexp
	nifCondRexp   *regexp.Regexp
	setCondRexp   *regexp.Regexp
	unsetCondRexp *regexp.Regexp
}

func newRoomParser() roomParser {
	roomInfoRexp := regexp.MustCompile(`(?m)^# ([A-Za-z0-9_-]+) ([\w .,!/]+)$`)
	exitRexp := regexp.MustCompile("(?m)^=> ([\\w]+) ({([\\+-~!\\w ]+)} )?([^\n{}]+)$")
	setCondRexp := regexp.MustCompile(`\+([\w]+)`)
	unsetCondRexp := regexp.MustCompile(`-([\w]+)`)
	ifCondRexp := regexp.MustCompile(`~([\w]+)`)
	nifCondRexp := regexp.MustCompile(`!([\w]+)`)

	return roomParser{
		roomInfoRexp:  roomInfoRexp,
		exitRexp:      exitRexp,
		ifCondRexp:    ifCondRexp,
		setCondRexp:   setCondRexp,
		unsetCondRexp: unsetCondRexp,
		nifCondRexp:   nifCondRexp,
	}
}

func (rp *roomParser) findRoomInfo(room string) (gamemanager.Room, error) {
	infoMatch := rp.roomInfoRexp.FindStringSubmatch(room)
	if infoMatch == nil || len(infoMatch) != 3 {
		return gamemanager.Room{}, fmt.Errorf("%w: %v", ErrInvalidRoomHeader, infoMatch)
	}

	return gamemanager.Room{
		ID:          infoMatch[1],
		Name:        infoMatch[2],
		Exits:       []gamemanager.Exit{},
		Description: "",
	}, nil
}

func (rp *roomParser) removeInfoFromDesc(room string) string {
	return rp.roomInfoRexp.ReplaceAllString(room, "")
}

func (rp *roomParser) processExits(room string) ([]gamemanager.Exit, error) {
	roomExits := []gamemanager.Exit{}

	exitMatch := rp.exitRexp.FindAllStringSubmatch(room, -1)
	for _, exitStr := range exitMatch {
		if exitStr[1] == "" || exitStr[4] == "" {
			return roomExits, fmt.Errorf("%w: %s", ErrInvalidExitDescription, exitStr)
		}

		thisExit := gamemanager.Exit{
			Destination:    exitStr[1],
			Description:    exitStr[4],
			SetCondition:   "",
			UnsetCondition: "",
			IfCondition:    "",
			NotCondition:   "",
		}

		if exitStr[3] != "" {
			thisExit.SetCondition = getMatchOrDefault(rp.setCondRexp, exitStr[3])
			thisExit.UnsetCondition = getMatchOrDefault(rp.unsetCondRexp, exitStr[3])
			thisExit.IfCondition = getMatchOrDefault(rp.ifCondRexp, exitStr[3])
			thisExit.NotCondition = getMatchOrDefault(rp.nifCondRexp, exitStr[3])
		}

		roomExits = append(roomExits, thisExit)
	}

	return roomExits, nil
}

func (rp *roomParser) removeExitsFromDesc(room string) string {
	return rp.exitRexp.ReplaceAllString(room, "")
}

func (rp *roomParser) getRoomDescription(room string) string {
	room = rp.removeInfoFromDesc(room)
	room = rp.removeExitsFromDesc(room)
	room = strings.TrimSpace(room)

	return room
}

func getMatchOrDefault(rexp *regexp.Regexp, input string) string {
	setOpt := rexp.FindStringSubmatch(input)
	if len(setOpt) > 1 {
		return setOpt[1]
	}

	return ""
}

M pkg/gamemanager/room.go => pkg/gamemanager/room.go +0 -7
@@ 1,7 1,5 @@
package gamemanager

import "fmt"

// Room contains information about a room and its Exits.
type Room struct {
	ID          string `yaml:"room_id" json:"room_id" binding:"uuid"`


@@ 12,7 10,6 @@ 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"`
	SetCondition   string `yaml:"set_condition,omitempty" json:"set_condition"`


@@ 26,14 23,10 @@ func (r Room) filterExits(gameState GameState) []Exit {

	for _, e := range r.Exits {
		if e.IfCondition != "" && !gameState.ConditionMet(e.IfCondition) {
			fmt.Printf("Cant use exit %s due to not meeting condition\n", e.ID)

			continue
		}

		if e.NotCondition != "" && gameState.ConditionMet(e.NotCondition) {
			fmt.Printf("Can't use exit %s due to meeting condition\n", e.ID)

			continue
		}