]> git.draconx.ca Git - rrace.git/blob - src/game.c
eefafe5ac40e16d93df0581a51fc23a46539202b
[rrace.git] / src / game.c
1 /*
2  * Slide puzzle core game logic
3  * Copyright © 2022 Nick Bowler
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17  *
18  * The RNG implementation is adapted from xoshiro256** and splitmix64
19  * by David Blackman and Sebastiano Vigna, originally distributed under
20  * the Creative Commons Zero public domain dedication.
21  */
22
23 #include <config.h>
24 #include <limits.h>
25 #include <string.h>
26 #include <time.h>
27 #include <gethrxtime.h>
28 #include "game.h"
29
30 #define B64(x) ((x) & 0xffffffffffffffff)
31
32 /* Rotate val left by n bits.  The behaviour is undefined if n is zero. */
33 static unsigned long long rot_left64(unsigned long long val, int n)
34 {
35         return B64( (val << n) | (val >> (64 - n)) );
36 }
37
38 static unsigned long long xoshiro256ss(unsigned long long *s)
39 {
40         unsigned long long tmp, ret;
41
42         ret = B64(rot_left64(B64(s[1]*5), 7) * 9);
43         tmp = B64(s[1] << 17);
44
45         s[2] ^= s[0];
46         s[3] ^= s[1];
47         s[1] ^= s[2];
48         s[0] ^= s[3];
49         s[2] ^= tmp;
50         s[3] = rot_left64(s[3], 45);
51
52         return ret;
53 }
54
55 static unsigned long long splitmix64(unsigned long long *state)
56 {
57         unsigned long long z;
58
59         z = B64(*state += 0x9e3779b97f4a7c15);
60         z = B64((z ^ (z >> 30)) * 0xbf58476d1ce4e5b9);
61         z = B64((z ^ (z >> 27)) * 0x94d049bb133111eb);
62
63         return z ^ (z >> 31);
64 }
65
66 static unsigned long long rng_state[4];
67
68 static int rng_is_seeded(void)
69 {
70         return rng_state[0] || rng_state[1] || rng_state[2] || rng_state[3];
71 }
72
73 /* Calculate the least power of two greater than val, minus 1. */
74 static unsigned rng_mask(unsigned val)
75 {
76         val |= val >> 1;
77         val |= val >> 2;
78         val |= val >> 4;
79         val |= val >> 8;
80
81         if (UINT_MAX >= 65536)
82                 val |= val >> 16;
83
84         return val;
85 }
86
87 /* Return a random integer uniformly on the closed interval [0, limit-1] */
88 static unsigned rng_uniform_int(unsigned max)
89 {
90         unsigned mask = rng_mask(max-1);
91         unsigned long long val;
92
93         do {
94                 val = ( xoshiro256ss(rng_state) >> 32 ) & mask;
95         } while (val >= max);
96
97         return val;
98 }
99
100 static void shuffle(unsigned char *tiles, unsigned n)
101 {
102         unsigned i, j;
103
104         for (i = 1; i < n; i++) {
105                 unsigned char tmp;
106
107                 j = rng_uniform_int(i+1);
108                 tmp = tiles[i];
109                 tiles[i] = tiles[j];
110                 tiles[j] = tmp;
111         }
112 }
113
114 void game_reseed(unsigned long long seed)
115 {
116         rng_state[0] = splitmix64(&seed);
117         rng_state[1] = splitmix64(&seed);
118         rng_state[2] = splitmix64(&seed);
119         rng_state[3] = splitmix64(&seed);
120 }
121
122 #define assign_tile(planes, tile, bit) do { \
123         0[planes] |= (((tile)>>0) & 1ul) << (bit); \
124         1[planes] |= (((tile)>>1) & 1ul) << (bit); \
125         2[planes] |= (((tile)>>2) & 1ul) << (bit); \
126 } while (0)
127
128 void game_reset(struct board *board)
129 {
130         unsigned char tiles[25];
131         unsigned i;
132
133         if (!rng_is_seeded()) {
134                 unsigned long long seed;
135
136                 seed = time(NULL);
137                 seed += gethrxtime();
138                 seed += seed << 32;
139
140                 game_reseed(seed);
141         }
142
143         for (i = 0; i < 24; i++) {
144                 tiles[i] = (i%6) + 1;
145         }
146
147         shuffle(tiles, 24);
148         memset(board->goal, 0, sizeof board->goal);
149
150         /*
151          * Goal bitmap has 2-tile "gaps" between rows; it doesn't matter what
152          * these are set to and since we have a random permutation it doesn't
153          * matter which of the 24 nonempty tiles we pick for the actual goal.
154          */
155         for (i = 0; i < 13; i++) {
156                 assign_tile(board->goal, tiles[i], i);
157         }
158
159         tiles[24] = TILE_EMPTY;
160         shuffle(tiles, 25);
161         memset(board->game, 0, sizeof board->game);
162
163         for (i = 0; i < 25; i++) {
164                 assign_tile(board->game, tiles[i], i);
165
166                 if (tiles[i] == TILE_EMPTY) {
167                         board->game[3] = (1ul<<i);
168                         board->x = i%5;
169                         board->y = i/5;
170                 }
171         }
172
173 /* Uncomment to make every game stupidly easy -- winnable in 1 move */
174 #if 0
175         /* Force empty space to the border */
176         if (board_position(board->x, board->y) & GOAL_MASK) {
177                 switch (rng_uniform_int(4)) {
178                 case 0: game_do_move(board, board->x, 0); break;
179                 case 1: game_do_move(board, board->x, 4); break;
180                 case 2: game_do_move(board, 0, board->y); break;
181                 case 3: game_do_move(board, 4, board->y); break;
182                 }
183         }
184
185         /* Force goal to match the current board */
186         for (i = 0; i < 3; i++) {
187                 board->goal[i] = (board->game[i] & GOAL_MASK) >> GOAL_SHIFT;
188         }
189
190         /* Move empty space back to the centre */
191         if (board->x == 0 || board->x == 4) {
192                 game_do_move(board, 1+rng_uniform_int(3), board->y);
193         }
194
195         if (board->y == 0 || board->y == 4) {
196                 game_do_move(board, board->x, 1+rng_uniform_int(3));
197         }
198 #endif
199 }
200
201 uint_fast32_t game_do_move(struct board *board, int x, int y)
202 {
203         int bx = board->x, by = board->y;
204         uint_least32_t ret = 0, mask, val[4];
205         int i, shl, shr;
206
207         if ((bx != x) == (by != y))
208                 return 0;
209
210         if (bx == x) {
211                 mask = board_mask_v(x, by, y);
212                 shr = 5*(by < y);
213                 shl = 5*(by > y);
214         } else {
215                 mask = board_mask_h(y, bx, x);
216                 shr = bx < x;
217                 shl = bx > x;
218         }
219
220         for (i = 0; i < 4; i++) {
221                 board->game[i] ^= (val[i] = board->game[i] & mask);
222                 board->game[i] |= val[i] << shl >> shr;
223                 ret |= board->game[i] ^ val[i];
224         }
225
226         board->x = x;
227         board->y = y;
228
229         return mask & ret;
230 }
231
232 uint_fast32_t game_check_goal(struct board *board)
233 {
234         uint_least32_t *game = board->game;
235         uint_least16_t *goal = board->goal;
236         uint_least32_t mask = 0;
237         int i;
238
239         for (i = 0; i < 3; i++)
240                 mask |= game[i] ^ ((0ul+goal[i]) << GOAL_SHIFT);
241         return mask & GOAL_MASK;
242 }
243
244 void game_begin(struct board *board)
245 {
246         board->time_start = gethrxtime();
247 }
248
249 int_fast32_t game_elapsed(struct board *board)
250 {
251         return (gethrxtime() - board->time_start) / 1000000;
252 }
253
254 int_fast32_t game_finish(struct board *board)
255 {
256         int_fast32_t t = game_elapsed(board);
257         int i;
258
259         for (i = 0; i < 4; i++) {
260                 board->game[i] &= GOAL_MASK;
261         }
262
263         /*
264          * Setting both x and y hole location to a bogus value will effectively
265          * disable the game since there will be no valid moves.
266          */
267         board->x = board->y = -1;
268
269         return t;
270 }