Skip to content

Commit 4ece249

Browse files
authored
Merge pull request #87 from Itangalo/1.4
1.4
2 parents eb6b0b3 + 8f99335 commit 4ece249

18 files changed

Lines changed: 616 additions & 57 deletions

000-simulation.gs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ function simulate(iterations = false, mod = false) {
2727

2828
let extraArguments = parseArguments(arguments, 2);
2929
log('Starting to build initial data.', 'system');
30-
let gameStateSeed = modules[module].buildInitialData(...extraArguments);
30+
let gameStateSeed = {};
31+
if (modules[module].buildInitialData)
32+
gameStateSeed = modules[module].buildInitialData(...extraArguments);
3133
log('Initial data complete.', 'system');
3234

3335
/**
@@ -70,8 +72,14 @@ function simulate(iterations = false, mod = false) {
7072
}
7173
}
7274

75+
// Only on the first iteration, run postBuild().
76+
if (iteration == 1 && modules[module].postBuild)
77+
modules[module].postBuild(...extraArguments);
78+
79+
7380
// Make any customized additional processing of the game state.
74-
modules[module].preIteration(...extraArguments);
81+
if (modules[module].preIteration)
82+
modules[module].preIteration(...extraArguments);
7583

7684
/**
7785
* Play the game until it is over.

200-helpersGeneral.gs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,17 +137,36 @@ function processValue(value, checkArrays = true) {
137137
* Helper functions handling arrays.
138138
*/
139139

140-
// Used for sorting an array of objects by an object property.
141-
function sortByProperty(objArray, property, ascending = true) {
140+
/**
141+
* Used for sorting an array of objects by an object property.
142+
*
143+
* @param {array} objArray: The array of objects to be sorted.
144+
* @param {string} property: The name of the property to sort by.
145+
* @param {boolean} ascending: Set to false to sort descending. Defaults to true.
146+
* @param {boolean} shuffleFirst: Set to true to shuffle the array before sorting, to avoid bias.
147+
*/
148+
function sortByProperty(objArray, property, ascending = true, shuffleFirst = false) {
149+
if (shuffleFirst)
150+
shuffle(objArray);
142151
if (ascending)
143152
objArray.sort((a, b) => a[property] > b[property] ? 1 : -1);
144153
else
145154
objArray.sort((a, b) => b[property] > a[property] ? 1 : -1);
146155
return objArray;
147156
}
148157

149-
// Used for sorting an array of objects by a sub property.
150-
function sortBySubProperty(objArray, property, subProperty, ascending = true) {
158+
/**
159+
* Used for sorting an array of objects by a sub property.
160+
*
161+
* @param {array} objArray: The array of objects to be sorted.
162+
* @param {string} property: The name of the property containing the sub property.
163+
* @param {string} property: The name of the sub property to sort by.
164+
* @param {boolean} ascending: Set to false to sort descending. Defaults to true.
165+
* @param {boolean} shuffleFirst: Set to true to shuffle the array before sorting, to avoid bias.
166+
*/
167+
function sortBySubProperty(objArray, property, subProperty, ascending = true, shuffleFirst = false) {
168+
if (shuffleFirst)
169+
shuffle(objArray);
151170
if (ascending)
152171
objArray.sort((a, b) => a[property][subProperty] > b[property][subProperty] ? 1 : -1);
153172
else
@@ -319,6 +338,13 @@ function getCache(key) {
319338
return BPTstatic.cache[key];
320339
}
321340

341+
/**
342+
* Returns a string with 'character' repeated 'length' times.
343+
*/
344+
function repString(length, character = ' ') {
345+
return new Array(length + 1).join(character);
346+
}
347+
322348
// Returns the agent with the matching id or false if none is found.
323349
function getAgentById(id) {
324350
if (!gameState.agents)

classes/120-Track.gs

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
* - assumePresent: If true, missing pawns are created when calling getPawn(pawnId). Defaults to true.
1313
* - loop: If true, the last space is followed by the first. Defaults to false.
1414
* - gridMovement: If true, possible movement is defined through connections on spaces. Defaults to false.
15+
* - connectRadius: If set to a numeric value, each space will connect to all spaces within that distance
16+
* unless source or target spaces have explicit connections set by the 'connectsTo' property.
1517
* - symmetricConnections: If true, any connections between spaces are assumed to go both ways.
1618
* Only relevant if gridMovement is true. Defaults to true.
1719
* - cacheGraph: If true, the created map of connections between spaces is stored between game iterations.
@@ -59,40 +61,60 @@ class Track {
5961
* Rebuilds track data. Needed when new spaces are added.
6062
*/
6163
rebuild() {
62-
// Build a graph of how the spaces connect, if advanced movement is used.
64+
// Map space IDs to indices, for quicker reference. And update space indices.
65+
this.spaceMapping = {};
66+
for (let i in this.spaces) {
67+
this.spaceMapping[this.spaces[i].id] = i;
68+
this.spaces[i].index = parseInt(i);
69+
}
70+
71+
// Build a graph of how the spaces connect, if grid movement is used.
6372
// Data used by the a-star algorithm, to find paths in the grid.
6473
if (this.gridMovement) {
74+
// Reset pawn paths.
75+
this.pawnPaths = {};
76+
77+
// Attempt to fetch cached data, if relevant.
6578
if (this.cacheGraph) {
6679
this.graph = getCache('track.' + this.id + '.grid');
6780
this.heuristic = getCache('track.' + this.id + '.heuristic');
6881
}
6982
else
7083
this.graph = false;
84+
85+
// Build new graph data, if necessary.
7186
if (!this.graph) {
7287
this.graph = [];
7388
for (let i = 0; i < this.spaces.length; i++)
7489
this.graph.push([]);
7590
for (let s of this.spaces) {
76-
for (let c of s.connectsTo) {
77-
let target = new ObjectFilter({id: c}).findFirstInArray(this.spaces);
78-
this.graph[s.index][target.index] = 1;
91+
let connected = [];
92+
// Use connectRadius, if appropriate.
93+
if (this.connectRadius && !s.connectsTo.length) {
94+
let filter = new ObjectFilter().or().addEqualsCondition({id: s.id}).addNotEmptyCondition('connectsTo');
95+
connected = this.getSpacesWithinRadius(s, this.connectRadius);
96+
filter.removeFromArray(connected);
97+
connected = this.convertSpaceData(connected, 'object', 'index');
98+
}
99+
// Otherwise, use explicit connections set on the space.
100+
else {
101+
connected = this.convertSpaceData(s.connectsTo, 'id', 'index');
102+
}
103+
// Connect to each relevant space, and the reverse if relevant.
104+
for (let targetIndex of connected) {
105+
this.graph[s.index][targetIndex] = 1;
79106
if (this.symmetricConnections)
80-
this.graph[target.index][s.index] = 1;
107+
this.graph[targetIndex][s.index] = 1;
81108
}
82109
}
83110
let row = Array(this.graph.length).fill(1);
84111
this.heuristic = Array(this.graph.length).fill(row);
85-
setCache('track.' + this.id + '.grid', this.graph);
86-
setCache('track.' + this.id + '.heuristic', this.heuristic);
112+
// Cache for future reference, if relevant.
113+
if (this.cacheGraph) {
114+
setCache('track.' + this.id + '.grid', this.graph);
115+
setCache('track.' + this.id + '.heuristic', this.heuristic);
116+
}
87117
}
88-
this.pawnPaths = {};
89-
}
90-
91-
// Map space IDs to indices, for quicker reference. And update space indices.
92-
this.spaceMapping = {};
93-
for (let i in this.spaces) {
94-
this.spaceMapping[this.spaces[i].id] = i;
95-
this.spaces[i].index = parseInt(i);
96118
}
97119
}
98120

@@ -265,6 +287,57 @@ class Track {
265287
return spaces.map(x => this.convertSpaceData(x, 'index', returnType));
266288
}
267289

290+
/**
291+
* Returns all the spaces within the radius of the given point.
292+
*
293+
* @param {object} point: An object with (at least) coordinate values for the point to search from.
294+
* @param {number} radius: A maximum distance from the point to search.
295+
* @param {string} shape: Set to 'square' to search a square instead of a circle.
296+
*
297+
* @return: An array of spaces, sorted by distance to the point.
298+
*/
299+
getSpacesWithinRadius(point, radius = 1, shape = 'circle') {
300+
// Get all spaces within the distance from the point, in a square.
301+
let filter = new ObjectFilter();
302+
for (let c of this.coordinates) {
303+
let value = {};
304+
value[c] = point[c] - radius;
305+
filter.addGreaterOrEqualCondition(value);
306+
value[c] = point[c] + radius;
307+
filter.addLessOrEqualCondition(value);
308+
}
309+
// Get the distance from the point for all these candidates.
310+
let spaces = [];
311+
for (let s of filter.applyOnArray(this.spaces)) {
312+
let distance = getDistance(point, s, this.coordinates);
313+
if (shape == 'square' || distance <= radius)
314+
spaces.push({
315+
space: s,
316+
distance: distance,
317+
});
318+
}
319+
// Sort by distance from the point.
320+
sortByProperty(spaces, 'distance', true, true)[0]['space'];
321+
return buildArrayWithProperty(spaces, 'space');
322+
}
323+
324+
/**
325+
* Returns the space closest to a given point.
326+
*
327+
* @param {object} point: An object with (at least) coordinate values for the point to search from.
328+
* @param {number} radius: A maximum distance from the point to search. Smaller number increases search speed.
329+
* @param {string} shape: Set to 'square' to search a square instead of a circle.
330+
*
331+
* @return {Space} The space object with coordinates closest to the given point. If several
332+
* equally near, one of these is selected randomly.
333+
*/
334+
getClosestSpace(point, radius = 1, shape = 'circle') {
335+
let spaceList = this.getSpacesWithinRadius(point, radius, shape);
336+
if (spaceList.length == 0)
337+
throw('No space is within search distance from the given point.');
338+
return spaceList[0];
339+
}
340+
268341
/**
269342
* Does an estimation of whether there is a line of sight between two spaces. If point coordinates
270343
* are provided, these will be used instead of spaces' coordinates.
@@ -497,12 +570,12 @@ class Pawn {
497570
if (this.id === undefined)
498571
throw('Pawns must have an id property set.');
499572
// Add the track name + pawn to any agent matching the pawn id.
500-
for (let a of gameState.agents) {
501-
if (this.id == a.id) {
502-
if (a[track.id] === undefined)
503-
a[track.id] = {};
504-
a[track.id].pawn = this;
505-
}
573+
let agent = getAgentById(this.id);
574+
if (agent) {
575+
if (agent[track.id] === undefined)
576+
agent[track.id] = {
577+
pawn: this,
578+
};
506579
}
507580

508581
this.track = track;

classes/150-ObjectFilter.gs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,24 @@ class ObjectFilter {
5959
return this;
6060
}
6161

62+
/**
63+
* Requires that the stated object property is undefined, null, false or an empty string/array.
64+
* Only property name is passed as condition.
65+
*/
66+
addEmptyCondition(condition) {
67+
this[this.addMode].push(new ConditionEmpty(this, condition));
68+
return this;
69+
}
70+
71+
/**
72+
* Requires that the stated object property is NOT undefined, null, false or an empty string/array.
73+
* Only property name is passed as condition.
74+
*/
75+
addNotEmptyCondition(condition) {
76+
this[this.addMode].push(new ConditionNotEmpty(this, condition));
77+
return this;
78+
}
79+
6280
/**
6381
* Requires that the stated object property is higher than the stated value.
6482
* On the form {a: 3}.
@@ -225,6 +243,44 @@ class ConditionNotEquals extends ConditionEquals {
225243
}
226244
}
227245

246+
/**
247+
* Class for 'empty' conditions in ObjectFilters.
248+
* Returns true if the given property is undefined, null, false, an empty array or an empty string.
249+
*
250+
* @param {object} conditionData: The name of the property which should be empty.
251+
*/
252+
class ConditionEmpty extends ConditionEquals {
253+
validateAndProcess(conditionData) {
254+
if (typeof(conditionData) != 'string')
255+
throw('The condition data must be a string.');
256+
this.property = conditionData;
257+
}
258+
259+
evaluate(obj) {
260+
if (compareObjects(obj[this.property], []))
261+
return true;
262+
if ([undefined, null, false, ''].includes(obj[this.property]))
263+
return true;
264+
return false;
265+
}
266+
}
267+
268+
/**
269+
* Class for 'not empty' conditions in ObjectFilters.
270+
* Returns false if the given property is undefined, null, false, an empty array or an empty string.
271+
*
272+
* @param {object} conditionData: The name of the property which should be empty.
273+
*/
274+
class ConditionNotEmpty extends ConditionEmpty {
275+
evaluate(obj) {
276+
if (compareObjects(obj[this.property], []))
277+
return false;
278+
if ([undefined, null, false, ''].includes(obj[this.property]))
279+
return false;
280+
return true;
281+
}
282+
}
283+
228284
/**
229285
* Class for greater-than conditions in ObjectFilters.
230286
*

0 commit comments

Comments
 (0)