1 /++
2 	Note: much of the functionality of gamehelpers was moved to [arsd.game] on May 3, 2020.
3 	If you used that code, change `import arsd.gamehelpers;` to `import arsd.game;` and add
4 	game.d to your build (in addition to gamehelpers.d; the new game.d still imports this module)
5 	and you should be good to go.
6 
7 	This module now builds on only [arsd.color] to provide additional algorithm functions
8 	and data types that are common in games.
9 
10 	History:
11 		Massive change on May 3, 2020 to move the previous flagship class out and to
12 		a new module, [arsd.game], to make this one lighter on dependencies, just
13 		containing helpers rather than a consolidated omnibus import.
14 +/
15 module arsd.gamehelpers;
16 
17 deprecated("change `import arsd.gamehelpers;` to `import arsd.game;`")
18 void* create2dWindow(string title, int width = 512, int height = 512) { return null; }
19 
20 deprecated("change `import arsd.gamehelpers;` to `import arsd.game;`")
21 class GameHelperBase {}
22 
23 import std.math;
24 
25 import arsd.color;
26 
27 // Some math helpers
28 
29 int nextPowerOfTwo(int v) {
30 	v--;
31 	v |= v >> 1;
32 	v |= v >> 2;
33 	v |= v >> 4;
34 	v |= v >> 8;
35 	v |= v >> 16;
36 	v++;
37 	return v;
38 }
39 
40 /++
41 	Calculates the cross product of <u1, u2, u3> and <v1, v2, v3>, putting the result in <s1, s2, s3>.
42 +/
43 void crossProduct(
44 	float u1, float u2, float u3,
45 	float v1, float v2, float v3,
46 	out float s1, out float s2, out float s3)
47 {
48 	s1 = u2 * v3 - u3 * v2;
49 	s2 = u3 * v1 - u1 * v3;
50 	s3 = u1 * v2 - u2 * v1;
51 }
52 
53 /++
54 	3D rotates (x, y, z) theta radians about the axis represented by unit-vector (u, v, w), putting the results in (s1, s2, s3).
55 
56 	For example, to rotate about the Y axis, pass (0, 1, 0) as (u, v, w).
57 +/
58 void rotateAboutAxis(
59 	float theta, // in RADIANS
60 	float x, float y, float z,
61 	float u, float v, float w,
62 	out float xp, out float yp, out float zp)
63 {
64 	xp = u * (u*x + v*y + w*z) * (1 - cos(theta)) + x * cos(theta) + (-w*y + v*z) * sin(theta);
65 	yp = v * (u*x + v*y + w*z) * (1 - cos(theta)) + y * cos(theta) + (w*x - u*z) * sin(theta);
66 	zp = w * (u*x + v*y + w*z) * (1 - cos(theta)) + z * cos(theta) + (-v*x + u*y) * sin(theta);
67 }
68 
69 /++
70 	2D rotates (rotatingX, rotatingY) theta radians about (originX, originY), putting the result in (xp, yp).
71 +/
72 void rotateAboutPoint(
73 	float theta, // in RADIANS
74 	float originX, float originY,
75 	float rotatingX, float rotatingY,
76 	out float xp, out float yp)
77 {
78 	if(theta == 0) {
79 		xp = rotatingX;
80 		yp = rotatingY;
81 		return;
82 	}
83 
84 	rotatingX -= originX;
85 	rotatingY -= originY;
86 
87 	float s = sin(theta);
88 	float c = cos(theta);
89 
90 	float x = rotatingX * c - rotatingY * s;
91 	float y = rotatingX * s + rotatingY * c;
92 
93 	xp = x + originX;
94 	yp = y + originY;
95 }
96 
97 /++
98 	Represents the four basic directions on a grid. You can conveniently use it like:
99 
100 	---
101 	Point pt = Point(5, 3);
102 	pt += Dir.N; // moves up
103 	---
104 
105 	The opposite direction btw can be gotten with `pt * -1`.
106 
107 	History:
108 		Added May 3, 2020
109 
110 		The `Direction` alias was added November 29, 2025
111 +/
112 enum Dir { N = Point(0, -1), S = Point(0, 1), W = Point(-1, 0), E = Point(1, 0) }
113 
114 /// ditto
115 alias Direction = Dir;
116 
117 /++
118 	The four directions as a static array so you can assign to a local variable
119 	then shuffle, etc.
120 
121 	History: Added May 3, 2020
122 +/
123 Point[4] directions() {
124 	with(Dir) return [N, S, W, E];
125 }
126 
127 /++
128 	A random value off [Dir].
129 
130 	History: Added May 3, 2020
131 +/
132 Point randomDirection() {
133 	import std.random;
134 	return directions()[uniform(0, 4)];
135 }
136 
137 /++
138 	Cycles through all the directions the given number of times. If you have
139 	one cycle, it goes through each direction once in a random order. With two
140 	cycles, it will move each direction twice, but in random order - can be
141 	W, W, N, E, S, S, N, E, for example; it will not do the cycles in order but
142 	upon completion will have gone through them all.
143 
144 	This can be convenient because if the character's movement is not constrained,
145 	it will always return back to where it started after a random movement.
146 
147 	Returns: an input range of [Point]s. Please note that the current version returns
148 	`Point[]`, but I reserve the right to change that in the future; I only promise
149 	input range capabilities.
150 
151 	History: Added May 3, 2020
152 +/
153 auto randomDirectionCycle(int cycleCount = 1) {
154 	Point[] all = new Point[](cycleCount * 4);
155 	foreach(c; 0 .. cycleCount)
156 		all[c * 4 .. c * 4 + 4] = directions()[];
157 
158 	import std.random;
159 	return all.randomShuffle;
160 }
161 
162 /++
163 	Represents a 2d grid like an array. To encapsulate the whole `[y*width + x]` thing.
164 
165 	History:
166 		Added May 3, 2020
167 +/
168 struct Grid(T) {
169 	private Size size_;
170 	private T[] array;
171 
172 	pure @safe nothrow:
173 
174 	/// Creates a new GC-backed array
175 	this(Size size) {
176 		this.size_ = size;
177 		array = new T[](size.area);
178 	}
179 
180 	/// ditto
181 	this(int width, int height) {
182 		this(Size(width, height));
183 	}
184 
185 	@nogc:
186 
187 	/// Wraps an existing array.
188 	this(T[] array, Size size) {
189 		assert(array.length == size.area);
190 		this.array = array;
191 		this.size_ = size;
192 	}
193 
194 	@property {
195 		///
196 		inout(Size) size() inout { return size_; }
197 		///
198 		int width() const { return size.width; }
199 		///
200 		int height() const { return size.height; }
201 	}
202 
203 	/// Slice operation gives a view into the underlying 1d array.
204 	inout(T)[] opIndex() inout {
205 		return array;
206 	}
207 
208 	///
209 	ref inout(T) opIndex(int x, int y) inout {
210 		return array[y * width + x];
211 	}
212 
213 	///
214 	ref inout(T) opIndex(const Point pt) inout {
215 		return this.opIndex(pt.x, pt.y);
216 	}
217 	// T[] opSlice
218 
219 	///
220 	bool inBounds(int x, int y) const {
221 		return x >= 0 && y >= 0 && x < width && y < height;
222 	}
223 
224 	///
225 	bool inBounds(const Point pt) const {
226 		return inBounds(pt.x, pt.y);
227 	}
228 
229 	/// Supports `if(point in grid) {}`
230 	bool opBinaryRight(string op : "in")(Point pt) const {
231 		return inBounds(pt);
232 	}
233 }
234 
235 /++
236 	Directions as a maskable bit flag.
237 
238 	History: Added May 3, 2020
239 +/
240 enum DirFlag : ubyte {
241 	N = 4,
242 	S = 8,
243 	W = 1,
244 	E = 2
245 }
246 
247 /++
248 	History: Added May 3, 2020
249 +/
250 DirFlag dirFlag(Dir dir) {
251 	assert(dir.x >= -1 && dir.x <= 1);
252 	assert(dir.y >= -1 && dir.y <= 1);
253 
254 
255 	/+
256 		(-1 + 3) / 2 = 2 / 2 = 1
257 		(1 + 3) / 2  = 4 / 2 = 2
258 
259 		So the al-gore-rhythm is
260 			(x + 3) / 2
261 				which is aka >> 1
262 			  or
263 			((y + 3) / 2) << 2
264 				which is aka >> 1 << 2 aka << 1
265 		So:
266 			1 = left
267 			2 = right
268 			4 = up
269 			8 = down
270 	+/
271 
272 	ubyte dirFlag;
273 	if(dir.x) dirFlag |= ((dir.x + 3) >> 1);
274 	if(dir.y) dirFlag |= ((dir.y + 3) << 1);
275 	return cast(DirFlag) dirFlag;
276 }
277 
278 // this is public but like i don't want do document it since it can so easily fail the asserts.
279 DirFlag dirFlag(Point dir) {
280 	return dirFlag(*cast(Dir*) &dir);
281 }
282 
283 /++
284 	Generates a maze.
285 
286 	The returned array is a grid of rooms, with a bit flag pattern of directions you can travel from each room. See [DirFlag] for bits.
287 
288 	History: Added May 3, 2020
289 +/
290 Grid!ubyte generateMaze(int mazeWidth, int mazeHeight) {
291 	import std.random;
292 
293 	Point[] cells;
294 	cells ~= Point(uniform(0, mazeWidth), uniform(0, mazeHeight));
295 
296 	auto grid = Grid!ubyte(mazeWidth, mazeHeight);
297 
298 	Point[4] directions = .directions;
299 
300 	while(cells.length) {
301 		auto index = cells.length - 1; // could also be 0 or uniform or whatever too
302 		Point p = cells[index];
303 		bool added;
304 		foreach(dir; directions[].randomShuffle) {
305 			auto n = p + dir;
306 			if(n !in grid)
307 				continue;
308 
309 			if(grid[n])
310 				continue;
311 
312 			grid[p] |= dirFlag(dir);
313 			grid[n] |= dirFlag(dir * -1);
314 
315 			cells ~= n;
316 
317 			added = true;
318 			break;
319 		}
320 
321 		if(!added) {
322 			foreach(i; index .. cells.length - 1)
323 				cells[index] = cells[index + 1];
324 			cells = cells[0 .. $-1];
325 		}
326 	}
327 
328 	return grid;
329 }
330 
331 
332 /++
333 	Implements the A* path finding algorithm on a grid.
334 
335 	Params:
336 		start = starting point on the grid
337 		goal = destination point on the grid
338 		size = size of the grid
339 		isPassable = used to determine if the tile at the given coordinates are passible
340 		d = weight function to the A* algorithm. If null, assumes all will be equal weight. Returned value must be greater than or equal to 1.
341 		h = heuristic function to the A* algorithm. Gives an estimation of how many steps away the goal is from the given point to speed up the search. If null, assumes "taxicab distance"; the number of steps based solely on distance without diagonal movement. If you want to disable this entirely, pass `p => 0`.
342 	Returns:
343 		A list of waypoints to reach the destination, or `null` if impossible. The waypoints are returned in reverse order, starting from the goal and working back to the start.
344 
345 		So to get to the goal from the starting point, follow the returned array in $(B backwards).
346 
347 		The waypoints will not necessarily include every step but also may not only list turns, but if you follow
348 		them you will get get to the destination.
349 
350 	Bugs:
351 		The current implementation uses more memory than it really has to; it will eat like 8 MB of scratch space RAM on a 512x512 grid.
352 
353 		It doesn't consider wraparound possible so it might ask you to go all the way around the world unnecessarily.
354 
355 	History:
356 		Added May 2, 2020.
357 +/
358 Point[] pathfind(Point start, Point goal, Size size, scope bool delegate(Point) isPassable, scope int delegate(Point, Point) d = null, scope int delegate(Point) h = null) {
359 
360 	Point[] reconstruct_path(scope Point[] cameFrom, Point current) {
361 		Point[] totalPath;
362 		totalPath ~= current;
363 
364 		auto cf = cameFrom[current.y * size.width + current.x];
365 		while(cf != Point(int.min, int.min)) {
366 			current = cf;
367 			cf = cameFrom[current.y * size.width + current.x];
368 			totalPath ~= current;
369 		}
370 		return totalPath;
371 	}
372 
373 	// weighting thing.....
374 	static int d_default(Point a, Point b) {
375 		return 1;
376 	}
377 
378 	if(d is null)
379 		d = (Point a, Point b) => d_default(a, b);
380 
381 	if(h is null)
382 		h = (Point a) { return abs(a.y - goal.x) + abs(a.y - goal.y); };
383 
384 	Point[] openSet = [start];
385 
386 	Point[] cameFrom = new Point[](size.area);
387 	cameFrom[] = Point(int.min, int.min);
388 
389 	int[] gScore = new int[](size.area);
390 	gScore[] = int.max;
391 	gScore[start.y * size.width + start.x] = 0;
392 
393 	int[] fScore = new int[](size.area);
394 	fScore[] = int.max;
395 	fScore[start.y * size.width + start.x] = h(start);
396 
397 	while(openSet.length) {
398 		Point current;
399 		size_t currentIdx;
400 		int currentFscore = int.max;
401 		foreach(idx, pt; openSet) {
402 			auto p = fScore[pt.y * size.width + pt.x];
403 			if(p <= currentFscore) {
404 				currentFscore = p;
405 				current = pt;
406 				currentIdx = idx;
407 			}
408 		}
409 
410 		if(current == goal) {
411 /+
412 import std.stdio;
413 foreach(y; 0 .. size.height)
414 	writefln("%(%02d,%)", gScore[y * size.width .. y * size.width + size.width]);
415 +/
416 			return reconstruct_path(cameFrom, current);
417 		}
418 
419 		openSet[currentIdx] = openSet[$-1];
420 		openSet = openSet[0 .. $-1];
421 
422 		Point[4] neighborsBuffer;
423 		int neighborsBufferLength = 0;
424 
425 
426 		// FIXME: would be kinda cool to make this a more generic graph traversal like for subway routes too
427 
428 		if(current.x + 1 < size.width && isPassable(current + Point(1, 0)))
429 			neighborsBuffer[neighborsBufferLength++] = current + Point(1, 0);
430 		if(current.x && isPassable(current + Point(-1, 0)))
431 			neighborsBuffer[neighborsBufferLength++] = current + Point(-1, 0);
432 		if(current.y && isPassable(current + Point(0, -1)))
433 			neighborsBuffer[neighborsBufferLength++] = current + Point(0, -1);
434 		if(current.y + 1 < size.height && isPassable(current + Point(0, 1)))
435 			neighborsBuffer[neighborsBufferLength++] = current + Point(0, 1);
436 
437 		foreach(neighbor; neighborsBuffer[0 .. neighborsBufferLength]) {
438 			auto tentative_gScore = gScore[current.y * size.width + current.x] + d(current, neighbor);
439 			if(tentative_gScore < gScore[neighbor.y * size.width + neighbor.x]) {
440 				cameFrom[neighbor.y * size.width + neighbor.x] = current;
441 				gScore[neighbor.y * size.width + neighbor.x] = tentative_gScore;
442 				fScore[neighbor.y * size.width + neighbor.x] = tentative_gScore + h(neighbor);
443 				// this linear thing might not be so smart after all
444 				bool found = false;
445 				foreach(o; openSet)
446 					if(o == neighbor) { found = true; break; }
447 				if(!found)
448 					openSet ~= neighbor;
449 			}
450 		}
451 	}
452 
453 	return null;
454 }