~nromdotcom/gemif

ref: 1bf434089d4531dcf6ce4735f644d52949571b1c gemif/pkg/gamemanager/gamemanager.go -rw-r--r-- 5.7 KiB
1bf43408Norm MacLennan First pass at conditionals 1 year, 1 month ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
package gamemanager

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
)

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

// GameManager contains configuration info about stories and managed GameState objects.
type GameManager struct {
	config config
}

// SpeculativeState is used to provide "what if" GameStates for next available actions.
type SpeculativeState struct {
	Description string
	StateToken  string
}

var (
	// ErrRoomNotFound occurs when the room doesn't exist in the given story.
	ErrRoomNotFound = errors.New("room not found")
	// ErrExitNotFound occurs when the exit doesn't exit in the given room.
	ErrExitNotFound = errors.New("game not found")
)

// New creates a new GameManager, loading all stories from a given path.
func New(path string) (*GameManager, error) {
	var config config
	if err := config.getStories(path); err != nil {
		return nil, fmt.Errorf("unable to load game: %w", err)
	}

	return &GameManager{config}, nil
}

// StartGame creates a new GameState representing the start of a given storyID.
func (gm *GameManager) StartGame(storyID string) (GameState, error) {
	return GameState{
		StoryID:     storyID,
		CurrentRoom: gm.config.Stories[storyID].Rooms[0].ID,
		Conditions:  []string{},
	}, nil
}

// GetStories lists all StoryMetadata for stories tracked by GameManager.
func (gm *GameManager) GetStories() []StoryMetadata {
	metadata := make([]StoryMetadata, len(gm.config.Stories))

	i := 0

	for _, e := range gm.config.Stories {
		metadata[i] = e.Metadata
		i++
	}

	return metadata
}

// GetRoomByID finds Room information based on room and story IDs.
func (gm *GameManager) GetRoomByID(storyID string, roomID string) (Room, error) {
	for i := range gm.config.Stories[storyID].Rooms {
		if gm.config.Stories[storyID].Rooms[i].ID == roomID {
			return gm.config.Stories[storyID].Rooms[i], nil
		}
	}

	return Room{}, fmt.Errorf("%w: %s", ErrRoomNotFound, roomID)
}

// UseExit mutates gameState based on the selected exitID.
func (gm *GameManager) UseExit(gameState GameState, exitID string) (GameState, error) {
	currentRoom, err := gm.GetRoomByID(gameState.StoryID, gameState.CurrentRoom)
	if err != nil {
		return GameState{}, fmt.Errorf("couldn't find room %s in story %s: %w", gameState.CurrentRoom, gameState.StoryID, err)
	}

	for i := range currentRoom.Exits {
		if currentRoom.Exits[i].ID == exitID {
			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
		}
	}

	return GameState{}, fmt.Errorf("%w: %s", ErrExitNotFound, exitID)
}

// DeserializeState converts a stateToken into a GameState.
func (gm *GameManager) DeserializeState(stateToken string) (GameState, error) {
	decodedString, decodeErr := base64.StdEncoding.DecodeString(stateToken)
	if decodeErr != nil {
		return GameState{}, fmt.Errorf("couldn't decode state token: %w", decodeErr)
	}

	var gameState GameState
	if unmarshallErr := json.Unmarshal(decodedString, &gameState); unmarshallErr != nil {
		return GameState{}, fmt.Errorf("failed to load state: %w", unmarshallErr)
	}

	return gameState, nil
}

// SerializeState generates a stateToken based on the given GameState.
func (gm *GameManager) SerializeState(gameState GameState) (string, error) {
	jsonBytes, marshallErr := json.Marshal(gameState)
	if marshallErr != nil {
		return "", fmt.Errorf("failed to save state: %w", marshallErr)
	}

	return base64.StdEncoding.EncodeToString(jsonBytes), nil
}

// ConstructSpeculativeStates makes a SpeculativeState from the active GameState and actions available in currentRoom.
func (gm *GameManager) ConstructSpeculativeStates(gameState GameState, currentRoom Room) ([]SpeculativeState, error) {
	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)
		}

		token, serializeErr := gm.SerializeState(newState)
		if serializeErr != nil {
			return speculativeStates, fmt.Errorf("couldn't serialize constructed state: %w", serializeErr)
		}

		speculativeStates[i] = SpeculativeState{Description: e.Description, StateToken: token}
	}

	return speculativeStates, nil
}

// ConstructStartingState generates a starting stateToken for each loaded story.
func (gm *GameManager) ConstructStartingState(stories []StoryMetadata) ([]SpeculativeState, error) {
	speculativeStates := make([]SpeculativeState, len(stories))

	for i, s := range stories {
		startState, startErr := gm.StartGame(s.ID)
		if startErr != nil {
			return speculativeStates, fmt.Errorf("could not generate starting state: %w", startErr)
		}

		startToken, serializeError := gm.SerializeState(startState)
		if serializeError != nil {
			return speculativeStates, fmt.Errorf("could not serialize state: %w", serializeError)
		}

		speculativeStates[i] = SpeculativeState{
			Description: fmt.Sprintf("%s by %s - %s", s.Name, s.Author, s.Description),
			StateToken:  startToken,
		}
	}

	return speculativeStates, nil
}

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