forked from pasky/pachi
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpatternsp.c
472 lines (417 loc) · 13.4 KB
/
patternsp.c
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
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
#define DEBUG
#include <assert.h>
#include <ctype.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include "board.h"
#include "debug.h"
#include "pattern.h"
#include "patternsp.h"
/* Mapping from point sequence to coordinate offsets (to determine
* coordinates relative to pattern center). The array is ordered
* in the gridcular metric order so that we can go through it
* and incrementally match spatial features in nested circles.
* Within one circle, coordinates are ordered by rows to keep
* good cache behavior. */
struct ptcoord ptcoords[MAX_PATTERN_AREA];
/* For each radius, starting index in ptcoords[]. */
unsigned int ptind[MAX_PATTERN_DIST + 2];
/* ptcoords[], ptind[] setup */
static void
ptcoords_init(void)
{
int i = 0; /* Indexing ptcoords[] */
/* First, center point. */
ptind[0] = ptind[1] = 0;
ptcoords[i].x = ptcoords[i].y = 0; i++;
for (int d = 2; d <= MAX_PATTERN_DIST; d++) {
ptind[d] = i;
/* For each y, examine all integer solutions
* of d = |x| + |y| + max(|x|, |y|). */
/* TODO: (Stern, 2006) uses a hand-modified
* circles that are finer for small d and more
* coarse for large d. */
for (short y = d / 2; y >= 0; y--) {
short x;
if (y > d / 3) {
/* max(|x|, |y|) = |y|, non-zero x */
x = d - y * 2;
if (x + y * 2 != d) continue;
} else {
/* max(|x|, |y|) = |x| */
/* Or, max(|x|, |y|) = |y| and x is zero */
x = (d - y) / 2;
if (x * 2 + y != d) continue;
}
assert((x > y ? x : y) + x + y == d);
ptcoords[i].x = x; ptcoords[i].y = y; i++;
if (x != 0) { ptcoords[i].x = -x; ptcoords[i].y = y; i++; }
if (y != 0) { ptcoords[i].x = x; ptcoords[i].y = -y; i++; }
if (x != 0 && y != 0) { ptcoords[i].x = -x; ptcoords[i].y = -y; i++; }
}
}
ptind[MAX_PATTERN_DIST + 1] = i;
#if 0
for (int d = 0; d <= MAX_PATTERN_DIST; d++) {
fprintf(stderr, "d=%d (%d) ", d, ptind[d]);
for (int j = ptind[d]; j < ptind[d + 1]; j++) {
fprintf(stderr, "%d,%d ", ptcoords[j].x, ptcoords[j].y);
}
fprintf(stderr, "\n");
}
#endif
}
/* Zobrist hashes used for points in patterns. */
hash_t pthashes[PTH__ROTATIONS][MAX_PATTERN_AREA][S_MAX];
static void
pthashes_init(void)
{
/* We need fixed hashes for all pattern-relative in
* all pattern users! This is a simple way to generate
* hopefully good ones. Park-Miller powa. :) */
/* We create a virtual board (centered at the sequence start),
* plant the hashes there, then pick them up into the sequence
* with correct coordinates. It would be possible to generate
* the sequence point hashes directly, but the rotations would
* make for enormous headaches. */
#define PATTERN_BOARD_SIZE ((MAX_PATTERN_DIST + 1) * (MAX_PATTERN_DIST + 1))
hash_t pthboard[PATTERN_BOARD_SIZE][4];
int pthbc = PATTERN_BOARD_SIZE / 2; // tengen coord
/* The magic numbers are tuned for minimal collisions. */
hash_t h1 = 0xd6d6d6d1;
hash_t h2 = 0xd6d6d6d2;
hash_t h3 = 0xd6d6d6d3;
hash_t h4 = 0xd6d6d6d4;
for (int i = 0; i < PATTERN_BOARD_SIZE; i++) {
pthboard[i][S_NONE] = (h1 = h1 * 16787);
pthboard[i][S_BLACK] = (h2 = h2 * 16823);
pthboard[i][S_WHITE] = (h3 = h3 * 16811 - 13);
pthboard[i][S_OFFBOARD] = (h4 = h4 * 16811);
}
/* Virtual board with hashes created, now fill
* pthashes[] with hashes for points in actual
* sequences, also considering various rotations. */
#define PTH_VMIRROR 1
#define PTH_HMIRROR 2
#define PTH_90ROT 4
for (int r = 0; r < PTH__ROTATIONS; r++) {
for (int i = 0; i < MAX_PATTERN_AREA; i++) {
/* Rotate appropriately. */
int rx = ptcoords[i].x;
int ry = ptcoords[i].y;
if (r & PTH_VMIRROR) ry = -ry;
if (r & PTH_HMIRROR) rx = -rx;
if (r & PTH_90ROT) {
int rs = rx; rx = -ry; ry = rs;
}
int bi = pthbc + ry * (MAX_PATTERN_DIST + 1) + rx;
/* Copy info. */
pthashes[r][i][S_NONE] = pthboard[bi][S_NONE];
pthashes[r][i][S_BLACK] = pthboard[bi][S_BLACK];
pthashes[r][i][S_WHITE] = pthboard[bi][S_WHITE];
pthashes[r][i][S_OFFBOARD] = pthboard[bi][S_OFFBOARD];
}
}
}
static void __attribute__((constructor))
spatial_init(void)
{
/* Initialization of various static data structures for
* fast pattern processing. */
ptcoords_init();
pthashes_init();
}
inline hash_t
spatial_hash(unsigned int rotation, struct spatial *s)
{
hash_t h = 0;
for (unsigned int i = 0; i < ptind[s->dist + 1]; i++) {
h ^= pthashes[rotation][i][spatial_point_at(*s, i)];
}
return h & spatial_hash_mask;
}
char *
spatial2str(struct spatial *s)
{
static char buf[1024];
for (unsigned int i = 0; i < ptind[s->dist + 1]; i++) {
buf[i] = stone2char(spatial_point_at(*s, i));
}
buf[ptind[s->dist + 1]] = 0;
return buf;
}
void
spatial_from_board(struct pattern_config *pc, struct spatial *s,
struct board *b, struct move *m)
{
assert(pc->spat_min > 0);
/* We record all spatial patterns black-to-play; simply
* reverse all colors if we are white-to-play. */
static enum stone bt_black[4] = { S_NONE, S_BLACK, S_WHITE, S_OFFBOARD };
static enum stone bt_white[4] = { S_NONE, S_WHITE, S_BLACK, S_OFFBOARD };
enum stone (*bt)[4] = m->color == S_WHITE ? &bt_white : &bt_black;
memset(s, 0, sizeof(*s));
for (unsigned int j = 0; j < ptind[pc->spat_max + 1]; j++) {
ptcoords_at(x, y, m->coord, b, j);
s->points[j / 4] |= (*bt)[board_atxy(b, x, y)] << ((j % 4) * 2);
}
s->dist = pc->spat_max;
}
/* Compare two spatials, allowing for differences up to isomorphism.
* True means the spatials are equivalent. */
static bool
spatial_cmp(struct spatial *s1, struct spatial *s2)
{
/* Quick preliminary check. */
if (s1->dist != s2->dist)
return false;
/* We could create complex transposition tables, but it seems most
* foolproof to just check if the sets of rotation hashes are the
* same for both. */
hash_t s1r[PTH__ROTATIONS];
for (unsigned int r = 0; r < PTH__ROTATIONS; r++)
s1r[r] = spatial_hash(r, s1);
for (unsigned int r = 0; r < PTH__ROTATIONS; r++) {
hash_t s2r = spatial_hash(r, s2);
for (unsigned int p = 0; p < PTH__ROTATIONS; p++)
if (s2r == s1r[p])
goto found_rot;
/* Rotation hash s2r does not correspond to s1r. */
return false;
found_rot:;
}
/* All rotation hashes of s2 occur in s1. Hopefully that
* indicates something. */
return true;
}
/* Spatial dict manipulation. */
static unsigned int
spatial_dict_addc(struct spatial_dict *dict, struct spatial *s)
{
/* Allocate space in 1024 blocks. */
#define SPATIALS_ALLOC 1024
if (!(dict->nspatials % SPATIALS_ALLOC)) {
dict->spatials = realloc(dict->spatials,
(dict->nspatials + SPATIALS_ALLOC)
* sizeof(*dict->spatials));
}
dict->spatials[dict->nspatials] = *s;
return dict->nspatials++;
}
bool
spatial_dict_addh(struct spatial_dict *dict, hash_t hash, unsigned int id)
{
if (dict->hash[hash]) {
if (dict->hash[hash] != id)
dict->collisions++;
} else {
dict->fills++;
}
dict->hash[hash] = id;
return true;
}
/* Spatial dictionary file format:
* /^#/ - comments
* INDEX RADIUS STONES HASH...
* INDEX: index in the spatial table
* RADIUS: @d of the pattern
* STONES: string of ".XO#" chars
* HASH...: space-separated 18bit hash-table indices for the pattern */
static void
spatial_dict_read(struct spatial_dict *dict, char *buf, bool hash)
{
/* XXX: We trust the data. Bad data will crash us. */
char *bufp = buf;
unsigned int index, radius;
index = strtoul(bufp, &bufp, 10);
radius = strtoul(bufp, &bufp, 10);
while (isspace(*bufp)) bufp++;
if (radius > MAX_PATTERN_DIST) {
/* Too large spatial, skip. */
struct spatial s = { .dist = 0 };
unsigned int id = spatial_dict_addc(dict, &s);
assert(id == index);
return;
}
/* Load the stone configuration. */
struct spatial s = { .dist = radius };
unsigned int sl = 0;
while (!isspace(*bufp)) {
s.points[sl / 4] |= char2stone(*bufp++) << ((sl % 4)*2);
sl++;
}
while (isspace(*bufp)) bufp++;
/* Sanity check. */
if (sl != ptind[s.dist + 1]) {
fprintf(stderr, "Spatial dictionary: Invalid number of stones (%d != %d) on this line: %s\n",
sl, ptind[radius + 1] - 1, buf);
exit(EXIT_FAILURE);
}
/* Add to collection. */
unsigned int id = spatial_dict_addc(dict, &s);
assert(id == index);
/* Add to specified hash places. */
if (hash)
for (unsigned int r = 0; r < PTH__ROTATIONS; r++)
spatial_dict_addh(dict, spatial_hash(r, &s), id);
}
void
spatial_write(struct spatial_dict *dict, struct spatial *s, unsigned int id, FILE *f)
{
fprintf(f, "%d %d ", id, s->dist);
fputs(spatial2str(s), f);
for (unsigned int r = 0; r < PTH__ROTATIONS; r++) {
hash_t rhash = spatial_hash(r, s);
unsigned int id2 = dict->hash[rhash];
if (id2 != id) {
/* This hash does not belong to us. Decide whether
* we or the current owner is better owner. */
/* TODO: Compare also # of patternscan encounters? */
struct spatial *s2 = &dict->spatials[id2];
if (s2->dist < s->dist)
continue;
if (s2->dist == s->dist && id2 < id)
continue;
}
fprintf(f, " %"PRIhash"", spatial_hash(r, s));
}
fputc('\n', f);
}
static void
spatial_dict_load(struct spatial_dict *dict, FILE *f, bool hash)
{
char buf[1024];
while (fgets(buf, sizeof(buf), f)) {
if (buf[0] == '#') continue;
spatial_dict_read(dict, buf, hash);
}
if (DEBUGL(1)) {
fprintf(stderr, "Loaded spatial dictionary of %d patterns.\n", dict->nspatials);
if (hash)
spatial_dict_hashstats(dict);
}
}
void
spatial_dict_hashstats(struct spatial_dict *dict)
{
/* m hash size, n number of patterns; is zobrist universal hash?
*
* Not so rigorous analysis, but it should give a good approximation:
* Probability of empty bucket is (1-1/m)^n ~ e^(-n/m)
* Probability of non-empty bucket is 1-e^(-n/m)
* Expected number of non-empty buckets is m*(1-e^(-n/m))
* Number of collisions is n-m*(1-e^(-n/m)). */
/* The result: Reality matches these expectations pretty well!
*
* Actual:
* Loaded spatial dictionary of 1064482 patterns.
* (Spatial dictionary hash: 513997 collisions (incl. repetitions), 11.88% (7970033/67108864) fill rate).
*
* Theoretical:
* m = 2^26
* n <= 8*1064482 (some patterns may have some identical rotations)
* n = 513997+7970033 = 8484030 should be the correct number
* n-m*(1-e^(-n/m)) = 514381
*
* To verify, make sure to turn patternprob off (e.g. use
* -e patternscan), since it will insert a pattern multiple times,
* multiplying the reported number of collisions. */
unsigned long buckets = (sizeof(dict->hash) / sizeof(dict->hash[0]));
fprintf(stderr, "\t(Spatial dictionary hash: %d collisions (incl. repetitions), %.2f%% (%d/%lu) fill rate).\n",
dict->collisions,
(double) dict->fills * 100 / buckets,
dict->fills, buckets);
}
void
spatial_dict_writeinfo(struct spatial_dict *dict, FILE *f)
{
/* New file. First, create a comment describing order
* of points in the array. This is just for purposes
* of external tools, Pachi never interprets it itself. */
fprintf(f, "# Pachi spatial patterns dictionary v1.0 maxdist %d\n",
MAX_PATTERN_DIST);
for (unsigned int d = 0; d <= MAX_PATTERN_DIST; d++) {
fprintf(f, "# Point order: d=%d ", d);
for (unsigned int j = ptind[d]; j < ptind[d + 1]; j++) {
fprintf(f, "%d,%d ", ptcoords[j].x, ptcoords[j].y);
}
fprintf(f, "\n");
}
}
/* We try to avoid needlessly reloading spatial dictionary
* since it may take rather long time. */
static struct spatial_dict *cached_dict;
const char *spatial_dict_filename = "patterns.spat";
struct spatial_dict *
spatial_dict_init(bool will_append, bool hash)
{
if (cached_dict && !will_append)
return cached_dict;
FILE *f = fopen(spatial_dict_filename, "r");
if (!f && !will_append) {
if (DEBUGL(1))
fprintf(stderr, "No spatial dictionary, will not match spatial pattern features.\n");
return NULL;
}
struct spatial_dict *dict = calloc2(1, sizeof(*dict));
/* We create a dummy record for index 0 that we will
* never reference. This is so that hash value 0 can
* represent "no value". */
struct spatial dummy = { .dist = 0 };
spatial_dict_addc(dict, &dummy);
if (f) {
spatial_dict_load(dict, f, hash);
fclose(f); f = NULL;
} else {
assert(will_append);
}
cached_dict = dict;
return dict;
}
unsigned int
spatial_dict_put(struct spatial_dict *dict, struct spatial *s, hash_t h)
{
/* We avoid spatial_dict_get() here, since we want to ignore radius
* differences - we have custom collision detection. */
unsigned int id = dict->hash[h];
if (id > 0) {
/* Is this the same or isomorphous spatial? */
if (spatial_cmp(s, &dict->spatials[id]))
return id;
/* Look a bit harder - perhaps one of our rotations still
* points at the correct spatial. */
for (unsigned int r = 0; r < PTH__ROTATIONS; r++) {
hash_t rhash = spatial_hash(r, s);
unsigned int rid = dict->hash[rhash];
/* No match means we definitely aren't stored yet. */
if (!rid)
break;
if (id != rid && spatial_cmp(s, &dict->spatials[rid])) {
/* Yay, this is us! */
if (DEBUGL(3))
fprintf(stderr, "Repeated collision %d vs %d\n", id, rid);
id = rid;
/* Point the hashes back to us. */
goto hash_store;
}
}
if (DEBUGL(1))
fprintf(stderr, "Collision %d vs %d\n", id, dict->nspatials);
id = 0;
/* dict->collisions++; gets done by addh */
}
/* Add new pattern! */
id = spatial_dict_addc(dict, s);
if (DEBUGL(4)) {
fprintf(stderr, "new spat %d(%d) %s <%"PRIhash"> ", id, s->dist, spatial2str(s), h);
for (unsigned int r = 0; r < 8; r++)
fprintf(stderr,"[%"PRIhash"] ", spatial_hash(r, s));
fprintf(stderr, "\n");
}
/* Store new pattern in the hash. */
hash_store:
for (unsigned int r = 0; r < PTH__ROTATIONS; r++)
spatial_dict_addh(dict, spatial_hash(r, s), id);
return id;
}