This repository has been archived by the owner on Aug 17, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathworld.dart
313 lines (273 loc) · 10.1 KB
/
world.dart
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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
library stranded.world;
import 'dart:collection';
import 'package:edgehead/fractal_stories/action.dart';
import 'package:edgehead/fractal_stories/util/throw_if_duplicate.dart';
import 'package:quiver/core.dart';
import 'action_record.dart';
import 'actor.dart';
import 'item.dart';
import 'room.dart';
import 'situation.dart';
class WorldState {
final Set<Actor> actors;
final Set<Item> items;
/// The global flags and counters that make up the state of the world.
///
/// Use this as sparingly as possible. Flags can be often avoided by checking
/// for specific past actions (has "kill_jack" been performed? then Jack is
/// dead and we don't need that flag).
///
/// This object must have a hash code that is value-based so that globals
/// with the same state have the same [Object.hashCode].
dynamic global;
/// A 'memory' of actions. The queue is in reverse chronological order,
/// with the latest record at the beginning of the queue. This is because
/// we often want to process only the latest (or the latest few) records.
final Queue<ActionRecord> actionRecords;
final Set<Room> rooms;
/// A stack of situations. The top-most (first) one is the [currentSituation].
///
/// This is a push-down automaton.
final List<Situation> situations;
/// The age of this WorldState. Every 'turn', this number increases by one.
int time;
/// This is normally `null` but is set to the current action when that action
/// is being applied.
///
/// This is so that, for example, descriptions of Rooms can access this
/// information and provide text according to how the Room is being reached.
///
/// TODO: This should exist in the 'mutable' WorldState while the immutable
/// worldstate shouldn't have this. Since, today, immutability of
/// WorldState outside action application is merely a convention, we
/// have it here.
Action currentAction;
WorldState(Iterable<Actor> actors, Iterable<Room> rooms,
Situation startingSituation, this.global)
: actors = new Set<Actor>.from(actors),
actionRecords = new Queue<ActionRecord>(),
items = new Set<Item>(),
rooms = new Set<Room>.from(rooms),
situations = new List<Situation>.from([startingSituation]),
time = 0;
/// Creates a deep clone of [other]. TODO: use BuiltValue here as well.
WorldState.duplicate(WorldState other)
: actors = new Set<Actor>(),
actionRecords = new Queue<ActionRecord>(),
items = new Set<Item>(),
rooms = new Set(),
situations = new List(),
global = other.global {
actors.addAll(other.actors);
actionRecords.addAll(other.actionRecords.where((rec) => rec.time > (time ?? 0) - 200));
items.addAll(other.items);
rooms.addAll(other.rooms);
situations.addAll(other.situations);
assert(!hasDuplicities(actors.map((a) => a.id)));
assert(!hasDuplicities(rooms.map((r) => r.name)));
time = other.time;
assert(
other.currentAction == null,
"currentAction should only be non-null "
"during application of an action.");
}
Situation get currentSituation =>
situations.isNotEmpty ? situations.last : null;
@override
int get hashCode {
return hash4(hashObjects(actors), hashObjects(actionRecords),
hashObjects(situations), hash2(time, global));
}
@override
bool operator ==(Object o) => o is WorldState && hashCode == o.hashCode;
/// Returns `true` if any action in the action records (past actions)
/// has the [Action.name] of [actionName] and was ever performed
/// _successfully_.
bool actionHasBeenPerformedSuccessfully(String actionName) {
var records = getActionRecords(actionName: actionName, wasSuccess: true);
for (var _ in records) {
return true;
}
return false;
}
/// Returns `true` if any action in the action records (past actions)
/// has the [Action.name] of [actionName].
///
/// This returns `true` regardless of success or failure.
bool actionHasBeenPerformed(String actionName) {
var records = getActionRecords(actionName: actionName);
for (var _ in records) {
return true;
}
return false;
}
/// Returns `true` if action that satisfies [actionNamePattern] is currently
/// being performed.
///
/// This is for cases like when a room description needs to know whether
/// player has arrived via a cart or on foot.
bool actionIsBeingPerformed(Pattern actionNamePattern) {
if (currentAction == null) return false;
return currentAction.name.contains(actionNamePattern);
}
/// Returns `true` if action with [Action.name] equal to [name] has never been
/// used, regardless if it was used successfully or not.
bool actionNeverUsed(String name) {
return timeSinceLastActionRecord(actionName: name) == null;
}
void elapseSituationTime(int situationId) {
int index = _findSituationIndex(situationId);
if (index == null) {
throw new StateError("Tried to elapseSituationTime of situation "
"id=$situationId that doesn't exist in situations ($situations).");
}
situations[index] = situations[index].elapseTime();
}
void elapseTime() {
time += 1;
}
/// Returns a lazy iterable of action records conforming to specified input,
/// in reverse chronological order.
///
/// When none of the named parameters is provided, all [actionRecords] are
/// returned.
Iterable<ActionRecord> getActionRecords(
{String actionName,
Actor protagonist,
Actor sufferer,
bool wasSuccess,
bool wasAggressive}) {
Iterable<ActionRecord> records = actionRecords;
if (actionName != null) {
records = records.where((rec) => rec.actionName == actionName);
}
if (protagonist != null) {
records = records.where((rec) => rec.protagonist == protagonist.id);
}
if (sufferer != null) {
records = records.where((rec) => rec.sufferers.contains(sufferer.id));
}
if (wasSuccess != null) {
records = records.where((rec) => rec.wasSuccess == wasSuccess);
}
if (wasAggressive != null) {
records = records.where((rec) => rec.wasAggressive == wasAggressive);
}
return records.take(100);
}
Actor getActorById(int id) {
assert(actors.where((actor) => actor.id == id).length > 0,
"No actor of id=$id in world: $this.");
assert(actors.where((actor) => actor.id == id).length < 2,
"Too many actors of id=$id in world: $this.");
return actors.singleWhere((actor) => actor.id == id);
}
Room getRoomByName(String roomName) {
assert(
rooms.any((room) => room.name == roomName),
"Room with name $roomName not defined.\n"
"Rooms: ${rooms.map((r) => r.name).join(', ')}.\n"
"Current world: $this.");
return rooms.singleWhere((room) => room.name == roomName);
}
Situation getSituationById(int situationId) {
int index = _findSituationIndex(situationId);
if (index == null) return null;
return situations[index];
}
/// Returns the [Situation] of the provided [situationName] that is highest
/// on the [situations] stack.
///
/// [situationName] must correspond to [Situation.name]. So this is really
/// finding situations by type. That is why this is a generic method. The
/// intended use can look like this:
///
/// var s = world.getSituationByName<SomeSituation>("SomeSituation");
///
/// Throws an [ArgumentError] if there is no Situation of that name on the
/// stack.
T getSituationByName<T extends Situation>(String situationName) {
for (int i = situations.length - 1; i >= 0; i--) {
if (situations[i].name == situationName) {
return situations[i] as T;
}
}
throw new ArgumentError("No situation with name=$situationName found.");
}
bool hasAliveActor(int actorId) {
var actor =
actors.firstWhere((actor) => actor.id == actorId, orElse: () => null);
if (actor == null) return false;
return actor.isAlive;
}
void popSituation() {
var removal = situations.last;
removal.onPop(this);
// The onPop function could have added another situation to the stack,
// so we can't use `situations.removeLast()`.
situations.remove(removal);
}
void popSituationsUntil(String situationName) {
while (situations.isNotEmpty && situations.last.name != situationName) {
popSituation();
}
if (situations.isEmpty) {
throw new ArgumentError("Tried to pop situations until $situationName "
"but none was found in stack.");
}
}
void pushSituation(Situation situation) {
situations.add(situation);
}
void replaceSituationById<T extends Situation>(int id, T updatedSituation) {
int index = _findSituationIndex(id);
if (index == null) {
throw new ArgumentError("Situation with id $id does not "
"exist in $situations");
}
situations[index] = updatedSituation;
}
bool situationExists(int situationId) =>
_findSituationIndex(situationId) != null;
/// Returns number of turns since an [ActionRecord] that conforms to
/// the specified named parameters was performed.
///
/// Returns `null` when such a record doesn't exist.
int timeSinceLastActionRecord(
{String actionName,
Actor protagonist,
Actor sufferer,
bool wasSuccess,
bool wasAggressive}) {
var records = getActionRecords(
actionName: actionName,
protagonist: protagonist,
sufferer: sufferer,
wasSuccess: wasSuccess,
wasAggressive: wasAggressive);
for (var record in records) {
return time - record.time;
}
return null;
}
@override
String toString() => "World<${actors.toSet()}>";
void updateActorById(int id, void updates(ActorBuilder b)) {
var original = getActorById(id);
var updated = original.rebuild(updates);
actors.remove(original);
actors.add(updated);
}
/// Returns the index at which the [Situation] with [situationId] resides
/// in the [situations] list.
int _findSituationIndex(int situationId) {
int index;
for (int i = 0; i < situations.length; i++) {
if (situations[i].id == situationId) {
index = i;
break;
}
}
return index;
}
}