| 1 | /* |
| 2 | * blackbox.c: implementation of 'Black Box'. |
| 3 | */ |
| 4 | |
| 5 | #include <stdio.h> |
| 6 | #include <stdlib.h> |
| 7 | #include <string.h> |
| 8 | #include <assert.h> |
| 9 | #include <ctype.h> |
| 10 | #include <math.h> |
| 11 | |
| 12 | #include "puzzles.h" |
| 13 | |
| 14 | #define PREFERRED_TILE_SIZE 32 |
| 15 | #define FLASH_FRAME 0.2F |
| 16 | |
| 17 | /* Terminology, for ease of reading various macros scattered about the place. |
| 18 | * |
| 19 | * The 'arena' is the inner area where the balls are placed. This is |
| 20 | * indexed from (0,0) to (w-1,h-1) but its offset in the grid is (1,1). |
| 21 | * |
| 22 | * The 'range' (firing range) is the bit around the edge where |
| 23 | * the lasers are fired from. This is indexed from 0 --> (2*(w+h) - 1), |
| 24 | * starting at the top left ((1,0) on the grid) and moving clockwise. |
| 25 | * |
| 26 | * The 'grid' is just the big array containing arena and range; |
| 27 | * locations (0,0), (0,w+1), (h+1,w+1) and (h+1,0) are unused. |
| 28 | */ |
| 29 | |
| 30 | enum { |
| 31 | COL_BACKGROUND, COL_COVER, COL_LOCK, |
| 32 | COL_TEXT, COL_FLASHTEXT, |
| 33 | COL_HIGHLIGHT, COL_LOWLIGHT, COL_GRID, |
| 34 | COL_BALL, COL_WRONG, COL_BUTTON, |
| 35 | COL_LASER, COL_DIMLASER, |
| 36 | NCOLOURS |
| 37 | }; |
| 38 | |
| 39 | struct game_params { |
| 40 | int w, h; |
| 41 | int minballs, maxballs; |
| 42 | }; |
| 43 | |
| 44 | static game_params *default_params(void) |
| 45 | { |
| 46 | game_params *ret = snew(game_params); |
| 47 | |
| 48 | ret->w = ret->h = 8; |
| 49 | ret->minballs = ret->maxballs = 5; |
| 50 | |
| 51 | return ret; |
| 52 | } |
| 53 | |
| 54 | static const game_params blackbox_presets[] = { |
| 55 | { 5, 5, 3, 3 }, |
| 56 | { 8, 8, 5, 5 }, |
| 57 | { 8, 8, 3, 6 }, |
| 58 | { 10, 10, 5, 5 }, |
| 59 | { 10, 10, 4, 10 } |
| 60 | }; |
| 61 | |
| 62 | static int game_fetch_preset(int i, char **name, game_params **params) |
| 63 | { |
| 64 | char str[80]; |
| 65 | game_params *ret; |
| 66 | |
| 67 | if (i < 0 || i >= lenof(blackbox_presets)) |
| 68 | return FALSE; |
| 69 | |
| 70 | ret = snew(game_params); |
| 71 | *ret = blackbox_presets[i]; |
| 72 | |
| 73 | if (ret->minballs == ret->maxballs) |
| 74 | sprintf(str, "%dx%d, %d balls", |
| 75 | ret->w, ret->h, ret->minballs); |
| 76 | else |
| 77 | sprintf(str, "%dx%d, %d-%d balls", |
| 78 | ret->w, ret->h, ret->minballs, ret->maxballs); |
| 79 | |
| 80 | *name = dupstr(str); |
| 81 | *params = ret; |
| 82 | return TRUE; |
| 83 | } |
| 84 | |
| 85 | static void free_params(game_params *params) |
| 86 | { |
| 87 | sfree(params); |
| 88 | } |
| 89 | |
| 90 | static game_params *dup_params(game_params *params) |
| 91 | { |
| 92 | game_params *ret = snew(game_params); |
| 93 | *ret = *params; /* structure copy */ |
| 94 | return ret; |
| 95 | } |
| 96 | |
| 97 | static void decode_params(game_params *params, char const *string) |
| 98 | { |
| 99 | char const *p = string; |
| 100 | game_params *defs = default_params(); |
| 101 | |
| 102 | *params = *defs; free_params(defs); |
| 103 | |
| 104 | while (*p) { |
| 105 | switch (*p++) { |
| 106 | case 'w': |
| 107 | params->w = atoi(p); |
| 108 | while (*p && isdigit((unsigned char)*p)) p++; |
| 109 | break; |
| 110 | |
| 111 | case 'h': |
| 112 | params->h = atoi(p); |
| 113 | while (*p && isdigit((unsigned char)*p)) p++; |
| 114 | break; |
| 115 | |
| 116 | case 'm': |
| 117 | params->minballs = atoi(p); |
| 118 | while (*p && isdigit((unsigned char)*p)) p++; |
| 119 | break; |
| 120 | |
| 121 | case 'M': |
| 122 | params->maxballs = atoi(p); |
| 123 | while (*p && isdigit((unsigned char)*p)) p++; |
| 124 | break; |
| 125 | |
| 126 | default: |
| 127 | ; |
| 128 | } |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | static char *encode_params(game_params *params, int full) |
| 133 | { |
| 134 | char str[256]; |
| 135 | |
| 136 | sprintf(str, "w%dh%dm%dM%d", |
| 137 | params->w, params->h, params->minballs, params->maxballs); |
| 138 | return dupstr(str); |
| 139 | } |
| 140 | |
| 141 | static config_item *game_configure(game_params *params) |
| 142 | { |
| 143 | config_item *ret; |
| 144 | char buf[80]; |
| 145 | |
| 146 | ret = snewn(4, config_item); |
| 147 | |
| 148 | ret[0].name = "Width"; |
| 149 | ret[0].type = C_STRING; |
| 150 | sprintf(buf, "%d", params->w); |
| 151 | ret[0].sval = dupstr(buf); |
| 152 | ret[0].ival = 0; |
| 153 | |
| 154 | ret[1].name = "Height"; |
| 155 | ret[1].type = C_STRING; |
| 156 | sprintf(buf, "%d", params->h); |
| 157 | ret[1].sval = dupstr(buf); |
| 158 | ret[1].ival = 0; |
| 159 | |
| 160 | ret[2].name = "No. of balls"; |
| 161 | ret[2].type = C_STRING; |
| 162 | if (params->minballs == params->maxballs) |
| 163 | sprintf(buf, "%d", params->minballs); |
| 164 | else |
| 165 | sprintf(buf, "%d-%d", params->minballs, params->maxballs); |
| 166 | ret[2].sval = dupstr(buf); |
| 167 | ret[2].ival = 0; |
| 168 | |
| 169 | ret[3].name = NULL; |
| 170 | ret[3].type = C_END; |
| 171 | ret[3].sval = NULL; |
| 172 | ret[3].ival = 0; |
| 173 | |
| 174 | return ret; |
| 175 | } |
| 176 | |
| 177 | static game_params *custom_params(config_item *cfg) |
| 178 | { |
| 179 | game_params *ret = snew(game_params); |
| 180 | |
| 181 | ret->w = atoi(cfg[0].sval); |
| 182 | ret->h = atoi(cfg[1].sval); |
| 183 | |
| 184 | /* Allow 'a-b' for a range, otherwise assume a single number. */ |
| 185 | if (sscanf(cfg[2].sval, "%d-%d", &ret->minballs, &ret->maxballs) < 2) |
| 186 | ret->minballs = ret->maxballs = atoi(cfg[2].sval); |
| 187 | |
| 188 | return ret; |
| 189 | } |
| 190 | |
| 191 | static char *validate_params(game_params *params, int full) |
| 192 | { |
| 193 | if (params->w < 2 || params->h < 2) |
| 194 | return "Width and height must both be at least two"; |
| 195 | /* next one is just for ease of coding stuff into 'char' |
| 196 | * types, and could be worked around if required. */ |
| 197 | if (params->w > 255 || params->h > 255) |
| 198 | return "Widths and heights greater than 255 are not supported"; |
| 199 | if (params->minballs > params->maxballs) |
| 200 | return "Minimum number of balls may not be greater than maximum"; |
| 201 | if (params->minballs >= params->w * params->h) |
| 202 | return "Too many balls to fit in grid"; |
| 203 | return NULL; |
| 204 | } |
| 205 | |
| 206 | /* |
| 207 | * We store: width | height | ball1x | ball1y | [ ball2x | ball2y | [...] ] |
| 208 | * all stored as unsigned chars; validate_params has already |
| 209 | * checked this won't overflow an 8-bit char. |
| 210 | * Then we obfuscate it. |
| 211 | */ |
| 212 | |
| 213 | static char *new_game_desc(game_params *params, random_state *rs, |
| 214 | char **aux, int interactive) |
| 215 | { |
| 216 | int nballs = params->minballs, i; |
| 217 | char *grid, *ret; |
| 218 | unsigned char *bmp; |
| 219 | |
| 220 | if (params->maxballs > params->minballs) |
| 221 | nballs += random_upto(rs, params->maxballs - params->minballs + 1); |
| 222 | |
| 223 | grid = snewn(params->w*params->h, char); |
| 224 | memset(grid, 0, params->w * params->h * sizeof(char)); |
| 225 | |
| 226 | bmp = snewn(nballs*2 + 2, unsigned char); |
| 227 | memset(bmp, 0, (nballs*2 + 2) * sizeof(unsigned char)); |
| 228 | |
| 229 | bmp[0] = params->w; |
| 230 | bmp[1] = params->h; |
| 231 | |
| 232 | for (i = 0; i < nballs; i++) { |
| 233 | int x, y; |
| 234 | |
| 235 | do { |
| 236 | x = random_upto(rs, params->w); |
| 237 | y = random_upto(rs, params->h); |
| 238 | } while (grid[y*params->w + x]); |
| 239 | |
| 240 | grid[y*params->w + x] = 1; |
| 241 | |
| 242 | bmp[(i+1)*2 + 0] = x; |
| 243 | bmp[(i+1)*2 + 1] = y; |
| 244 | } |
| 245 | sfree(grid); |
| 246 | |
| 247 | obfuscate_bitmap(bmp, (nballs*2 + 2) * 8, FALSE); |
| 248 | ret = bin2hex(bmp, nballs*2 + 2); |
| 249 | sfree(bmp); |
| 250 | |
| 251 | return ret; |
| 252 | } |
| 253 | |
| 254 | static char *validate_desc(game_params *params, char *desc) |
| 255 | { |
| 256 | int nballs, dlen = strlen(desc), i; |
| 257 | unsigned char *bmp; |
| 258 | char *ret; |
| 259 | |
| 260 | /* the bitmap is 2+(nballs*2) long; the hex version is double that. */ |
| 261 | nballs = ((dlen/2)-2)/2; |
| 262 | |
| 263 | if (dlen < 4 || dlen % 4 || |
| 264 | nballs < params->minballs || nballs > params->maxballs) |
| 265 | return "Game description is wrong length"; |
| 266 | |
| 267 | bmp = hex2bin(desc, nballs*2 + 2); |
| 268 | obfuscate_bitmap(bmp, (nballs*2 + 2) * 8, TRUE); |
| 269 | ret = "Game description is corrupted"; |
| 270 | /* check general grid size */ |
| 271 | if (bmp[0] != params->w || bmp[1] != params->h) |
| 272 | goto done; |
| 273 | /* check each ball will fit on that grid */ |
| 274 | for (i = 0; i < nballs; i++) { |
| 275 | int x = bmp[(i+1)*2 + 0], y = bmp[(i+1)*2 + 1]; |
| 276 | if (x < 0 || y < 0 || x >= params->w || y >= params->h) |
| 277 | goto done; |
| 278 | } |
| 279 | ret = NULL; |
| 280 | |
| 281 | done: |
| 282 | sfree(bmp); |
| 283 | return ret; |
| 284 | } |
| 285 | |
| 286 | #define BALL_CORRECT 0x01 |
| 287 | #define BALL_GUESS 0x02 |
| 288 | #define BALL_LOCK 0x04 |
| 289 | |
| 290 | #define LASER_FLAGMASK 0xf800 |
| 291 | #define LASER_OMITTED 0x0800 |
| 292 | #define LASER_REFLECT 0x1000 |
| 293 | #define LASER_HIT 0x2000 |
| 294 | #define LASER_WRONG 0x4000 |
| 295 | #define LASER_FLASHED 0x8000 |
| 296 | #define LASER_EMPTY (~0) |
| 297 | |
| 298 | struct game_state { |
| 299 | int w, h, minballs, maxballs, nballs, nlasers; |
| 300 | unsigned int *grid; /* (w+2)x(h+2), to allow for laser firing range */ |
| 301 | unsigned int *exits; /* one per laser */ |
| 302 | int done; /* user has finished placing his own balls. */ |
| 303 | int laserno; /* number of next laser to be fired. */ |
| 304 | int nguesses, reveal, justwrong, nright, nwrong, nmissed; |
| 305 | }; |
| 306 | |
| 307 | #define GRID(s,x,y) ((s)->grid[(y)*((s)->w+2) + (x)]) |
| 308 | |
| 309 | #define RANGECHECK(s,x) ((x) >= 0 && (x) <= (s)->nlasers) |
| 310 | |
| 311 | /* specify numbers because they must match array indexes. */ |
| 312 | enum { DIR_UP = 0, DIR_RIGHT = 1, DIR_DOWN = 2, DIR_LEFT = 3 }; |
| 313 | |
| 314 | struct offset { int x, y; }; |
| 315 | |
| 316 | static const struct offset offsets[] = { |
| 317 | { 0, -1 }, /* up */ |
| 318 | { 1, 0 }, /* right */ |
| 319 | { 0, 1 }, /* down */ |
| 320 | { -1, 0 } /* left */ |
| 321 | }; |
| 322 | |
| 323 | #ifdef DEBUGGING |
| 324 | static const char *dirstrs[] = { |
| 325 | "UP", "RIGHT", "DOWN", "LEFT" |
| 326 | }; |
| 327 | #endif |
| 328 | |
| 329 | static int range2grid(game_state *state, int rangeno, int *x, int *y, int *direction) |
| 330 | { |
| 331 | if (rangeno < 0) |
| 332 | return 0; |
| 333 | |
| 334 | if (rangeno < state->w) { |
| 335 | /* top row; from (1,0) to (w,0) */ |
| 336 | *x = rangeno + 1; |
| 337 | *y = 0; |
| 338 | *direction = DIR_DOWN; |
| 339 | return 1; |
| 340 | } |
| 341 | rangeno -= state->w; |
| 342 | if (rangeno < state->h) { |
| 343 | /* RHS; from (w+1, 1) to (w+1, h) */ |
| 344 | *x = state->w+1; |
| 345 | *y = rangeno + 1; |
| 346 | *direction = DIR_LEFT; |
| 347 | return 1; |
| 348 | } |
| 349 | rangeno -= state->h; |
| 350 | if (rangeno < state->w) { |
| 351 | /* bottom row; from (1, h+1) to (w, h+1); counts backwards */ |
| 352 | *x = (state->w - rangeno); |
| 353 | *y = state->h+1; |
| 354 | *direction = DIR_UP; |
| 355 | return 1; |
| 356 | } |
| 357 | rangeno -= state->w; |
| 358 | if (rangeno < state->h) { |
| 359 | /* LHS; from (0, 1) to (0, h); counts backwards */ |
| 360 | *x = 0; |
| 361 | *y = (state->h - rangeno); |
| 362 | *direction = DIR_RIGHT; |
| 363 | return 1; |
| 364 | } |
| 365 | return 0; |
| 366 | } |
| 367 | |
| 368 | static int grid2range(game_state *state, int x, int y, int *rangeno) |
| 369 | { |
| 370 | int ret, x1 = state->w+1, y1 = state->h+1; |
| 371 | |
| 372 | if (x > 0 && x < x1 && y > 0 && y < y1) return 0; /* in arena */ |
| 373 | if (x < 0 || x > y1 || y < 0 || y > y1) return 0; /* outside grid */ |
| 374 | |
| 375 | if ((x == 0 || x == x1) && (y == 0 || y == y1)) |
| 376 | return 0; /* one of 4 corners */ |
| 377 | |
| 378 | if (y == 0) { /* top line */ |
| 379 | ret = x - 1; |
| 380 | } else if (x == x1) { /* RHS */ |
| 381 | ret = y - 1 + state->w; |
| 382 | } else if (y == y1) { /* Bottom [and counts backwards] */ |
| 383 | ret = (state->w - x) + state->w + state->h; |
| 384 | } else { /* LHS [and counts backwards ] */ |
| 385 | ret = (state->h-y) + state->w + state->w + state->h; |
| 386 | } |
| 387 | *rangeno = ret; |
| 388 | debug(("grid2range: (%d,%d) rangeno = %d\n", x, y, ret)); |
| 389 | return 1; |
| 390 | } |
| 391 | |
| 392 | static game_state *new_game(midend *me, game_params *params, char *desc) |
| 393 | { |
| 394 | game_state *state = snew(game_state); |
| 395 | int dlen = strlen(desc), i; |
| 396 | unsigned char *bmp; |
| 397 | |
| 398 | state->minballs = params->minballs; |
| 399 | state->maxballs = params->maxballs; |
| 400 | state->nballs = ((dlen/2)-2)/2; |
| 401 | |
| 402 | bmp = hex2bin(desc, state->nballs*2 + 2); |
| 403 | obfuscate_bitmap(bmp, (state->nballs*2 + 2) * 8, TRUE); |
| 404 | |
| 405 | state->w = bmp[0]; state->h = bmp[1]; |
| 406 | state->nlasers = 2 * (state->w + state->h); |
| 407 | |
| 408 | state->grid = snewn((state->w+2)*(state->h+2), unsigned int); |
| 409 | memset(state->grid, 0, (state->w+2)*(state->h+2) * sizeof(unsigned int)); |
| 410 | |
| 411 | state->exits = snewn(state->nlasers, unsigned int); |
| 412 | memset(state->exits, LASER_EMPTY, state->nlasers * sizeof(unsigned int)); |
| 413 | |
| 414 | for (i = 0; i < state->nballs; i++) { |
| 415 | GRID(state, bmp[(i+1)*2 + 0]+1, bmp[(i+1)*2 + 1]+1) = BALL_CORRECT; |
| 416 | } |
| 417 | sfree(bmp); |
| 418 | |
| 419 | state->done = state->nguesses = state->reveal = state->justwrong = |
| 420 | state->nright = state->nwrong = state->nmissed = 0; |
| 421 | state->laserno = 1; |
| 422 | |
| 423 | return state; |
| 424 | } |
| 425 | |
| 426 | #define XFER(x) ret->x = state->x |
| 427 | |
| 428 | static game_state *dup_game(game_state *state) |
| 429 | { |
| 430 | game_state *ret = snew(game_state); |
| 431 | |
| 432 | XFER(w); XFER(h); |
| 433 | XFER(minballs); XFER(maxballs); |
| 434 | XFER(nballs); XFER(nlasers); |
| 435 | |
| 436 | ret->grid = snewn((ret->w+2)*(ret->h+2), unsigned int); |
| 437 | memcpy(ret->grid, state->grid, (ret->w+2)*(ret->h+2) * sizeof(unsigned int)); |
| 438 | ret->exits = snewn(ret->nlasers, unsigned int); |
| 439 | memcpy(ret->exits, state->exits, ret->nlasers * sizeof(unsigned int)); |
| 440 | |
| 441 | XFER(done); |
| 442 | XFER(laserno); |
| 443 | XFER(nguesses); |
| 444 | XFER(reveal); |
| 445 | XFER(justwrong); |
| 446 | XFER(nright); XFER(nwrong); XFER(nmissed); |
| 447 | |
| 448 | return ret; |
| 449 | } |
| 450 | |
| 451 | #undef XFER |
| 452 | |
| 453 | static void free_game(game_state *state) |
| 454 | { |
| 455 | sfree(state->exits); |
| 456 | sfree(state->grid); |
| 457 | sfree(state); |
| 458 | } |
| 459 | |
| 460 | static char *solve_game(game_state *state, game_state *currstate, |
| 461 | char *aux, char **error) |
| 462 | { |
| 463 | return dupstr("S"); |
| 464 | } |
| 465 | |
| 466 | static char *game_text_format(game_state *state) |
| 467 | { |
| 468 | return NULL; |
| 469 | } |
| 470 | |
| 471 | struct game_ui { |
| 472 | int flash_laserno; |
| 473 | int errors, newmove; |
| 474 | }; |
| 475 | |
| 476 | static game_ui *new_ui(game_state *state) |
| 477 | { |
| 478 | game_ui *ui = snew(game_ui); |
| 479 | ui->flash_laserno = LASER_EMPTY; |
| 480 | ui->errors = 0; |
| 481 | ui->newmove = FALSE; |
| 482 | return ui; |
| 483 | } |
| 484 | |
| 485 | static void free_ui(game_ui *ui) |
| 486 | { |
| 487 | sfree(ui); |
| 488 | } |
| 489 | |
| 490 | static char *encode_ui(game_ui *ui) |
| 491 | { |
| 492 | char buf[80]; |
| 493 | /* |
| 494 | * The error counter needs preserving across a serialisation. |
| 495 | */ |
| 496 | sprintf(buf, "E%d", ui->errors); |
| 497 | return dupstr(buf); |
| 498 | } |
| 499 | |
| 500 | static void decode_ui(game_ui *ui, char *encoding) |
| 501 | { |
| 502 | sscanf(encoding, "E%d", &ui->errors); |
| 503 | } |
| 504 | |
| 505 | static void game_changed_state(game_ui *ui, game_state *oldstate, |
| 506 | game_state *newstate) |
| 507 | { |
| 508 | /* |
| 509 | * If we've encountered a `justwrong' state as a result of |
| 510 | * actually making a move, increment the ui error counter. |
| 511 | */ |
| 512 | if (newstate->justwrong && ui->newmove) |
| 513 | ui->errors++; |
| 514 | ui->newmove = FALSE; |
| 515 | } |
| 516 | |
| 517 | #define OFFSET(gx,gy,o) do { \ |
| 518 | int off = (4 + (o) % 4) % 4; \ |
| 519 | (gx) += offsets[off].x; \ |
| 520 | (gy) += offsets[off].y; \ |
| 521 | } while(0) |
| 522 | |
| 523 | enum { LOOK_LEFT, LOOK_FORWARD, LOOK_RIGHT }; |
| 524 | |
| 525 | /* Given a position and a direction, check whether we can see a ball in front |
| 526 | * of us, or to our front-left or front-right. */ |
| 527 | static int isball(game_state *state, int gx, int gy, int direction, int lookwhere) |
| 528 | { |
| 529 | debug(("isball, (%d, %d), dir %s, lookwhere %s\n", gx, gy, dirstrs[direction], |
| 530 | lookwhere == LOOK_LEFT ? "LEFT" : |
| 531 | lookwhere == LOOK_FORWARD ? "FORWARD" : "RIGHT")); |
| 532 | OFFSET(gx,gy,direction); |
| 533 | if (lookwhere == LOOK_LEFT) |
| 534 | OFFSET(gx,gy,direction-1); |
| 535 | else if (lookwhere == LOOK_RIGHT) |
| 536 | OFFSET(gx,gy,direction+1); |
| 537 | else if (lookwhere != LOOK_FORWARD) |
| 538 | assert(!"unknown lookwhere"); |
| 539 | |
| 540 | debug(("isball, new (%d, %d)\n", gx, gy)); |
| 541 | |
| 542 | /* if we're off the grid (into the firing range) there's never a ball. */ |
| 543 | if (gx < 1 || gy < 1 || gx > state->h || gy > state->w) |
| 544 | return 0; |
| 545 | |
| 546 | if (GRID(state, gx,gy) & BALL_CORRECT) |
| 547 | return 1; |
| 548 | |
| 549 | return 0; |
| 550 | } |
| 551 | |
| 552 | static int fire_laser_internal(game_state *state, int x, int y, int direction) |
| 553 | { |
| 554 | int unused, lno, tmp; |
| 555 | |
| 556 | tmp = grid2range(state, x, y, &lno); |
| 557 | assert(tmp); |
| 558 | |
| 559 | /* deal with strange initial reflection rules (that stop |
| 560 | * you turning down the laser range) */ |
| 561 | |
| 562 | /* I've just chosen to prioritise instant-hit over instant-reflection; |
| 563 | * I can't find anywhere that gives me a definite algorithm for this. */ |
| 564 | if (isball(state, x, y, direction, LOOK_FORWARD)) { |
| 565 | debug(("Instant hit at (%d, %d)\n", x, y)); |
| 566 | return LASER_HIT; /* hit */ |
| 567 | } |
| 568 | |
| 569 | if (isball(state, x, y, direction, LOOK_LEFT) || |
| 570 | isball(state, x, y, direction, LOOK_RIGHT)) { |
| 571 | debug(("Instant reflection at (%d, %d)\n", x, y)); |
| 572 | return LASER_REFLECT; /* reflection */ |
| 573 | } |
| 574 | /* move us onto the grid. */ |
| 575 | OFFSET(x, y, direction); |
| 576 | |
| 577 | while (1) { |
| 578 | debug(("fire_laser: looping at (%d, %d) pointing %s\n", |
| 579 | x, y, dirstrs[direction])); |
| 580 | if (grid2range(state, x, y, &unused)) { |
| 581 | int exitno; |
| 582 | |
| 583 | tmp = grid2range(state, x, y, &exitno); |
| 584 | assert(tmp); |
| 585 | |
| 586 | return (lno == exitno ? LASER_REFLECT : exitno); |
| 587 | } |
| 588 | /* paranoia. This obviously should never happen */ |
| 589 | assert(!(GRID(state, x, y) & BALL_CORRECT)); |
| 590 | |
| 591 | if (isball(state, x, y, direction, LOOK_FORWARD)) { |
| 592 | /* we're facing a ball; send back a reflection. */ |
| 593 | debug(("Ball ahead of (%d, %d)", x, y)); |
| 594 | return LASER_HIT; /* hit */ |
| 595 | } |
| 596 | |
| 597 | if (isball(state, x, y, direction, LOOK_LEFT)) { |
| 598 | /* ball to our left; rotate clockwise and look again. */ |
| 599 | debug(("Ball to left; turning clockwise.\n")); |
| 600 | direction += 1; direction %= 4; |
| 601 | continue; |
| 602 | } |
| 603 | if (isball(state, x, y, direction, LOOK_RIGHT)) { |
| 604 | /* ball to our right; rotate anti-clockwise and look again. */ |
| 605 | debug(("Ball to rightl turning anti-clockwise.\n")); |
| 606 | direction += 3; direction %= 4; |
| 607 | continue; |
| 608 | } |
| 609 | /* ... otherwise, we have no balls ahead of us so just move one step. */ |
| 610 | debug(("No balls; moving forwards.\n")); |
| 611 | OFFSET(x, y, direction); |
| 612 | } |
| 613 | } |
| 614 | |
| 615 | static int laser_exit(game_state *state, int entryno) |
| 616 | { |
| 617 | int tmp, x, y, direction; |
| 618 | |
| 619 | tmp = range2grid(state, entryno, &x, &y, &direction); |
| 620 | assert(tmp); |
| 621 | |
| 622 | return fire_laser_internal(state, x, y, direction); |
| 623 | } |
| 624 | |
| 625 | static void fire_laser(game_state *state, int entryno) |
| 626 | { |
| 627 | int tmp, exitno, x, y, direction; |
| 628 | |
| 629 | tmp = range2grid(state, entryno, &x, &y, &direction); |
| 630 | assert(tmp); |
| 631 | |
| 632 | exitno = fire_laser_internal(state, x, y, direction); |
| 633 | |
| 634 | if (exitno == LASER_HIT || exitno == LASER_REFLECT) { |
| 635 | GRID(state, x, y) = state->exits[entryno] = exitno; |
| 636 | } else { |
| 637 | int newno = state->laserno++; |
| 638 | int xend, yend, unused; |
| 639 | tmp = range2grid(state, exitno, &xend, ¥d, &unused); |
| 640 | assert(tmp); |
| 641 | GRID(state, x, y) = GRID(state, xend, yend) = newno; |
| 642 | state->exits[entryno] = exitno; |
| 643 | state->exits[exitno] = entryno; |
| 644 | } |
| 645 | } |
| 646 | |
| 647 | /* Checks that the guessed balls in the state match up with the real balls |
| 648 | * for all possible lasers (i.e. not just the ones that the player might |
| 649 | * have already guessed). This is required because any layout with >4 balls |
| 650 | * might have multiple valid solutions. Returns non-zero for a 'correct' |
| 651 | * (i.e. consistent) layout. */ |
| 652 | static int check_guesses(game_state *state, int cagey) |
| 653 | { |
| 654 | game_state *solution, *guesses; |
| 655 | int i, x, y, n, unused, tmp; |
| 656 | int ret = 0; |
| 657 | |
| 658 | if (cagey) { |
| 659 | /* |
| 660 | * First, check that each laser the player has already |
| 661 | * fired is consistent with the layout. If not, show them |
| 662 | * one error they've made and reveal no further |
| 663 | * information. |
| 664 | * |
| 665 | * Failing that, check to see whether the player would have |
| 666 | * been able to fire any laser which distinguished the real |
| 667 | * solution from their guess. If so, show them one such |
| 668 | * laser and reveal no further information. |
| 669 | */ |
| 670 | guesses = dup_game(state); |
| 671 | /* clear out BALL_CORRECT on guess, make BALL_GUESS BALL_CORRECT. */ |
| 672 | for (x = 1; x <= state->w; x++) { |
| 673 | for (y = 1; y <= state->h; y++) { |
| 674 | GRID(guesses, x, y) &= ~BALL_CORRECT; |
| 675 | if (GRID(guesses, x, y) & BALL_GUESS) |
| 676 | GRID(guesses, x, y) |= BALL_CORRECT; |
| 677 | } |
| 678 | } |
| 679 | n = 0; |
| 680 | for (i = 0; i < guesses->nlasers; i++) { |
| 681 | if (guesses->exits[i] != LASER_EMPTY && |
| 682 | guesses->exits[i] != laser_exit(guesses, i)) |
| 683 | n++; |
| 684 | } |
| 685 | if (n) { |
| 686 | /* |
| 687 | * At least one of the player's existing lasers |
| 688 | * contradicts their ball placement. Pick a random one, |
| 689 | * highlight it, and return. |
| 690 | * |
| 691 | * A temporary random state is created from the current |
| 692 | * grid, so that repeating the same marking will give |
| 693 | * the same answer instead of a different one. |
| 694 | */ |
| 695 | random_state *rs = random_init((char *)guesses->grid, |
| 696 | (state->w+2)*(state->h+2) * |
| 697 | sizeof(unsigned int)); |
| 698 | n = random_upto(rs, n); |
| 699 | random_free(rs); |
| 700 | for (i = 0; i < guesses->nlasers; i++) { |
| 701 | if (guesses->exits[i] != LASER_EMPTY && |
| 702 | guesses->exits[i] != laser_exit(guesses, i) && |
| 703 | n-- == 0) { |
| 704 | state->exits[i] |= LASER_WRONG; |
| 705 | tmp = laser_exit(state, i); |
| 706 | if (RANGECHECK(state, tmp)) |
| 707 | state->exits[tmp] |= LASER_WRONG; |
| 708 | state->justwrong = TRUE; |
| 709 | free_game(guesses); |
| 710 | return 0; |
| 711 | } |
| 712 | } |
| 713 | } |
| 714 | n = 0; |
| 715 | for (i = 0; i < guesses->nlasers; i++) { |
| 716 | if (guesses->exits[i] == LASER_EMPTY && |
| 717 | laser_exit(state, i) != laser_exit(guesses, i)) |
| 718 | n++; |
| 719 | } |
| 720 | if (n) { |
| 721 | /* |
| 722 | * At least one of the player's unfired lasers would |
| 723 | * demonstrate their ball placement to be wrong. Pick a |
| 724 | * random one, highlight it, and return. |
| 725 | * |
| 726 | * A temporary random state is created from the current |
| 727 | * grid, so that repeating the same marking will give |
| 728 | * the same answer instead of a different one. |
| 729 | */ |
| 730 | random_state *rs = random_init((char *)guesses->grid, |
| 731 | (state->w+2)*(state->h+2) * |
| 732 | sizeof(unsigned int)); |
| 733 | n = random_upto(rs, n); |
| 734 | random_free(rs); |
| 735 | for (i = 0; i < guesses->nlasers; i++) { |
| 736 | if (guesses->exits[i] == LASER_EMPTY && |
| 737 | laser_exit(state, i) != laser_exit(guesses, i) && |
| 738 | n-- == 0) { |
| 739 | fire_laser(state, i); |
| 740 | state->exits[i] |= LASER_OMITTED; |
| 741 | tmp = laser_exit(state, i); |
| 742 | if (RANGECHECK(state, tmp)) |
| 743 | state->exits[tmp] |= LASER_OMITTED; |
| 744 | state->justwrong = TRUE; |
| 745 | free_game(guesses); |
| 746 | return 0; |
| 747 | } |
| 748 | } |
| 749 | } |
| 750 | free_game(guesses); |
| 751 | } |
| 752 | |
| 753 | /* duplicate the state (to solution) */ |
| 754 | solution = dup_game(state); |
| 755 | |
| 756 | /* clear out the lasers of solution */ |
| 757 | for (i = 0; i < solution->nlasers; i++) { |
| 758 | tmp = range2grid(solution, i, &x, &y, &unused); |
| 759 | assert(tmp); |
| 760 | GRID(solution, x, y) = 0; |
| 761 | solution->exits[i] = LASER_EMPTY; |
| 762 | } |
| 763 | |
| 764 | /* duplicate solution to guess. */ |
| 765 | guesses = dup_game(solution); |
| 766 | |
| 767 | /* clear out BALL_CORRECT on guess, make BALL_GUESS BALL_CORRECT. */ |
| 768 | for (x = 1; x <= state->w; x++) { |
| 769 | for (y = 1; y <= state->h; y++) { |
| 770 | GRID(guesses, x, y) &= ~BALL_CORRECT; |
| 771 | if (GRID(guesses, x, y) & BALL_GUESS) |
| 772 | GRID(guesses, x, y) |= BALL_CORRECT; |
| 773 | } |
| 774 | } |
| 775 | |
| 776 | /* for each laser (on both game_states), fire it if it hasn't been fired. |
| 777 | * If one has been fired (or received a hit) and another hasn't, we know |
| 778 | * the ball layouts didn't match and can short-circuit return. */ |
| 779 | for (i = 0; i < solution->nlasers; i++) { |
| 780 | if (solution->exits[i] == LASER_EMPTY) |
| 781 | fire_laser(solution, i); |
| 782 | if (guesses->exits[i] == LASER_EMPTY) |
| 783 | fire_laser(guesses, i); |
| 784 | } |
| 785 | |
| 786 | /* check each game_state's laser against the other; if any differ, return 0 */ |
| 787 | ret = 1; |
| 788 | for (i = 0; i < solution->nlasers; i++) { |
| 789 | tmp = range2grid(solution, i, &x, &y, &unused); |
| 790 | assert(tmp); |
| 791 | |
| 792 | if (solution->exits[i] != guesses->exits[i]) { |
| 793 | /* If the original state didn't have this shot fired, |
| 794 | * and it would be wrong between the guess and the solution, |
| 795 | * add it. */ |
| 796 | if (state->exits[i] == LASER_EMPTY) { |
| 797 | state->exits[i] = solution->exits[i]; |
| 798 | if (state->exits[i] == LASER_REFLECT || |
| 799 | state->exits[i] == LASER_HIT) |
| 800 | GRID(state, x, y) = state->exits[i]; |
| 801 | else { |
| 802 | /* add a new shot, incrementing state's laser count. */ |
| 803 | int ex, ey, newno = state->laserno++; |
| 804 | tmp = range2grid(state, state->exits[i], &ex, &ey, &unused); |
| 805 | assert(tmp); |
| 806 | GRID(state, x, y) = newno; |
| 807 | GRID(state, ex, ey) = newno; |
| 808 | } |
| 809 | state->exits[i] |= LASER_OMITTED; |
| 810 | } else { |
| 811 | state->exits[i] |= LASER_WRONG; |
| 812 | } |
| 813 | ret = 0; |
| 814 | } |
| 815 | } |
| 816 | if (ret == 0) goto done; |
| 817 | |
| 818 | /* fix up original state so the 'correct' balls end up matching the guesses, |
| 819 | * as we've just proved that they were equivalent. */ |
| 820 | for (x = 1; x <= state->w; x++) { |
| 821 | for (y = 1; y <= state->h; y++) { |
| 822 | if (GRID(state, x, y) & BALL_GUESS) |
| 823 | GRID(state, x, y) |= BALL_CORRECT; |
| 824 | else |
| 825 | GRID(state, x, y) &= ~BALL_CORRECT; |
| 826 | } |
| 827 | } |
| 828 | |
| 829 | done: |
| 830 | /* fill in nright and nwrong. */ |
| 831 | state->nright = state->nwrong = state->nmissed = 0; |
| 832 | for (x = 1; x <= state->w; x++) { |
| 833 | for (y = 1; y <= state->h; y++) { |
| 834 | int bs = GRID(state, x, y) & (BALL_GUESS | BALL_CORRECT); |
| 835 | if (bs == (BALL_GUESS | BALL_CORRECT)) |
| 836 | state->nright++; |
| 837 | else if (bs == BALL_GUESS) |
| 838 | state->nwrong++; |
| 839 | else if (bs == BALL_CORRECT) |
| 840 | state->nmissed++; |
| 841 | } |
| 842 | } |
| 843 | free_game(solution); |
| 844 | free_game(guesses); |
| 845 | state->reveal = 1; |
| 846 | return ret; |
| 847 | } |
| 848 | |
| 849 | #define TILE_SIZE (ds->tilesize) |
| 850 | |
| 851 | #define TODRAW(x) ((TILE_SIZE * (x)) + (TILE_SIZE / 2)) |
| 852 | #define FROMDRAW(x) (((x) - (TILE_SIZE / 2)) / TILE_SIZE) |
| 853 | |
| 854 | #define CAN_REVEAL(state) ((state)->nguesses >= (state)->minballs && \ |
| 855 | (state)->nguesses <= (state)->maxballs && \ |
| 856 | !(state)->reveal && !(state)->justwrong) |
| 857 | |
| 858 | struct game_drawstate { |
| 859 | int tilesize, crad, rrad, w, h; /* w and h to make macros work... */ |
| 860 | unsigned int *grid; /* as the game_state grid */ |
| 861 | int started, reveal; |
| 862 | int flash_laserno, isflash; |
| 863 | }; |
| 864 | |
| 865 | static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds, |
| 866 | int x, int y, int button) |
| 867 | { |
| 868 | int gx = -1, gy = -1, rangeno = -1; |
| 869 | enum { NONE, TOGGLE_BALL, TOGGLE_LOCK, FIRE, REVEAL, |
| 870 | TOGGLE_COLUMN_LOCK, TOGGLE_ROW_LOCK} action = NONE; |
| 871 | char buf[80], *nullret = NULL; |
| 872 | |
| 873 | if (button == LEFT_BUTTON || button == RIGHT_BUTTON) { |
| 874 | gx = FROMDRAW(x); |
| 875 | gy = FROMDRAW(y); |
| 876 | if (gx == 0 && gy == 0 && button == LEFT_BUTTON) |
| 877 | action = REVEAL; |
| 878 | if (gx >= 1 && gx <= state->w && gy >= 1 && gy <= state->h) { |
| 879 | if (button == LEFT_BUTTON) { |
| 880 | if (!(GRID(state, gx,gy) & BALL_LOCK)) |
| 881 | action = TOGGLE_BALL; |
| 882 | } else |
| 883 | action = TOGGLE_LOCK; |
| 884 | } |
| 885 | if (grid2range(state, gx, gy, &rangeno)) { |
| 886 | if (button == LEFT_BUTTON) |
| 887 | action = FIRE; |
| 888 | else if (gy == 0 || gy > state->h) |
| 889 | action = TOGGLE_COLUMN_LOCK; /* and use gx */ |
| 890 | else |
| 891 | action = TOGGLE_ROW_LOCK; /* and use gy */ |
| 892 | } |
| 893 | } else if (button == LEFT_RELEASE) { |
| 894 | ui->flash_laserno = LASER_EMPTY; |
| 895 | return ""; |
| 896 | } |
| 897 | |
| 898 | switch (action) { |
| 899 | case TOGGLE_BALL: |
| 900 | sprintf(buf, "T%d,%d", gx, gy); |
| 901 | break; |
| 902 | |
| 903 | case TOGGLE_LOCK: |
| 904 | sprintf(buf, "LB%d,%d", gx, gy); |
| 905 | break; |
| 906 | |
| 907 | case TOGGLE_COLUMN_LOCK: |
| 908 | sprintf(buf, "LC%d", gx); |
| 909 | break; |
| 910 | |
| 911 | case TOGGLE_ROW_LOCK: |
| 912 | sprintf(buf, "LR%d", gy); |
| 913 | break; |
| 914 | |
| 915 | case FIRE: |
| 916 | if (state->reveal && state->exits[rangeno] == LASER_EMPTY) |
| 917 | return nullret; |
| 918 | ui->flash_laserno = rangeno; |
| 919 | nullret = ""; |
| 920 | if (state->exits[rangeno] != LASER_EMPTY) |
| 921 | return ""; |
| 922 | sprintf(buf, "F%d", rangeno); |
| 923 | break; |
| 924 | |
| 925 | case REVEAL: |
| 926 | if (!CAN_REVEAL(state)) return nullret; |
| 927 | sprintf(buf, "R"); |
| 928 | break; |
| 929 | |
| 930 | default: |
| 931 | return nullret; |
| 932 | } |
| 933 | if (state->reveal) return nullret; |
| 934 | ui->newmove = TRUE; |
| 935 | return dupstr(buf); |
| 936 | } |
| 937 | |
| 938 | static game_state *execute_move(game_state *from, char *move) |
| 939 | { |
| 940 | game_state *ret = dup_game(from); |
| 941 | int gx = -1, gy = -1, rangeno = -1; |
| 942 | |
| 943 | if (ret->justwrong) { |
| 944 | int i; |
| 945 | ret->justwrong = FALSE; |
| 946 | for (i = 0; i < ret->nlasers; i++) |
| 947 | if (ret->exits[i] != LASER_EMPTY) |
| 948 | ret->exits[i] &= ~(LASER_OMITTED | LASER_WRONG); |
| 949 | } |
| 950 | |
| 951 | if (!strcmp(move, "S")) { |
| 952 | check_guesses(ret, FALSE); |
| 953 | return ret; |
| 954 | } |
| 955 | |
| 956 | if (from->reveal) goto badmove; |
| 957 | if (!*move) goto badmove; |
| 958 | |
| 959 | switch (move[0]) { |
| 960 | case 'T': |
| 961 | sscanf(move+1, "%d,%d", &gx, &gy); |
| 962 | if (gx < 1 || gy < 1 || gx > ret->w || gy > ret->h) |
| 963 | goto badmove; |
| 964 | if (GRID(ret, gx, gy) & BALL_GUESS) { |
| 965 | ret->nguesses--; |
| 966 | GRID(ret, gx, gy) &= ~BALL_GUESS; |
| 967 | } else { |
| 968 | ret->nguesses++; |
| 969 | GRID(ret, gx, gy) |= BALL_GUESS; |
| 970 | } |
| 971 | break; |
| 972 | |
| 973 | case 'F': |
| 974 | sscanf(move+1, "%d", &rangeno); |
| 975 | if (ret->exits[rangeno] != LASER_EMPTY) |
| 976 | goto badmove; |
| 977 | if (!RANGECHECK(ret, rangeno)) |
| 978 | goto badmove; |
| 979 | fire_laser(ret, rangeno); |
| 980 | break; |
| 981 | |
| 982 | case 'R': |
| 983 | if (ret->nguesses < ret->minballs || |
| 984 | ret->nguesses > ret->maxballs) |
| 985 | goto badmove; |
| 986 | check_guesses(ret, TRUE); |
| 987 | break; |
| 988 | |
| 989 | case 'L': |
| 990 | { |
| 991 | int lcount = 0; |
| 992 | if (strlen(move) < 2) goto badmove; |
| 993 | switch (move[1]) { |
| 994 | case 'B': |
| 995 | sscanf(move+2, "%d,%d", &gx, &gy); |
| 996 | if (gx < 1 || gy < 1 || gx > ret->w || gy > ret->h) |
| 997 | goto badmove; |
| 998 | GRID(ret, gx, gy) ^= BALL_LOCK; |
| 999 | break; |
| 1000 | |
| 1001 | #define COUNTLOCK do { if (GRID(ret, gx, gy) & BALL_LOCK) lcount++; } while (0) |
| 1002 | #define SETLOCKIF(c) do { \ |
| 1003 | if (lcount > (c)) GRID(ret, gx, gy) &= ~BALL_LOCK; \ |
| 1004 | else GRID(ret, gx, gy) |= BALL_LOCK; \ |
| 1005 | } while(0) |
| 1006 | |
| 1007 | case 'C': |
| 1008 | sscanf(move+2, "%d", &gx); |
| 1009 | if (gx < 1 || gx > ret->w) goto badmove; |
| 1010 | for (gy = 1; gy <= ret->h; gy++) { COUNTLOCK; } |
| 1011 | for (gy = 1; gy <= ret->h; gy++) { SETLOCKIF(ret->h/2); } |
| 1012 | break; |
| 1013 | |
| 1014 | case 'R': |
| 1015 | sscanf(move+2, "%d", &gy); |
| 1016 | if (gy < 1 || gy > ret->h) goto badmove; |
| 1017 | for (gx = 1; gx <= ret->w; gx++) { COUNTLOCK; } |
| 1018 | for (gx = 1; gx <= ret->w; gx++) { SETLOCKIF(ret->w/2); } |
| 1019 | break; |
| 1020 | |
| 1021 | #undef COUNTLOCK |
| 1022 | #undef SETLOCKIF |
| 1023 | |
| 1024 | default: |
| 1025 | goto badmove; |
| 1026 | } |
| 1027 | } |
| 1028 | break; |
| 1029 | |
| 1030 | default: |
| 1031 | goto badmove; |
| 1032 | } |
| 1033 | |
| 1034 | return ret; |
| 1035 | |
| 1036 | badmove: |
| 1037 | free_game(ret); |
| 1038 | return NULL; |
| 1039 | } |
| 1040 | |
| 1041 | /* ---------------------------------------------------------------------- |
| 1042 | * Drawing routines. |
| 1043 | */ |
| 1044 | |
| 1045 | static void game_compute_size(game_params *params, int tilesize, |
| 1046 | int *x, int *y) |
| 1047 | { |
| 1048 | /* Border is ts/2, to make things easier. |
| 1049 | * Thus we have (width) + 2 (firing range*2) + 1 (border*2) tiles |
| 1050 | * across, and similarly height + 2 + 1 tiles down. */ |
| 1051 | *x = (params->w + 3) * tilesize; |
| 1052 | *y = (params->h + 3) * tilesize; |
| 1053 | } |
| 1054 | |
| 1055 | static void game_set_size(drawing *dr, game_drawstate *ds, |
| 1056 | game_params *params, int tilesize) |
| 1057 | { |
| 1058 | ds->tilesize = tilesize; |
| 1059 | ds->crad = (tilesize-1)/2; |
| 1060 | ds->rrad = (3*tilesize)/8; |
| 1061 | } |
| 1062 | |
| 1063 | static float *game_colours(frontend *fe, game_state *state, int *ncolours) |
| 1064 | { |
| 1065 | float *ret = snewn(3 * NCOLOURS, float); |
| 1066 | int i; |
| 1067 | |
| 1068 | game_mkhighlight(fe, ret, COL_BACKGROUND, COL_HIGHLIGHT, COL_LOWLIGHT); |
| 1069 | |
| 1070 | ret[COL_BALL * 3 + 0] = 0.0F; |
| 1071 | ret[COL_BALL * 3 + 1] = 0.0F; |
| 1072 | ret[COL_BALL * 3 + 2] = 0.0F; |
| 1073 | |
| 1074 | ret[COL_WRONG * 3 + 0] = 1.0F; |
| 1075 | ret[COL_WRONG * 3 + 1] = 0.0F; |
| 1076 | ret[COL_WRONG * 3 + 2] = 0.0F; |
| 1077 | |
| 1078 | ret[COL_BUTTON * 3 + 0] = 0.0F; |
| 1079 | ret[COL_BUTTON * 3 + 1] = 1.0F; |
| 1080 | ret[COL_BUTTON * 3 + 2] = 0.0F; |
| 1081 | |
| 1082 | ret[COL_LASER * 3 + 0] = 1.0F; |
| 1083 | ret[COL_LASER * 3 + 1] = 0.0F; |
| 1084 | ret[COL_LASER * 3 + 2] = 0.0F; |
| 1085 | |
| 1086 | ret[COL_DIMLASER * 3 + 0] = 0.5F; |
| 1087 | ret[COL_DIMLASER * 3 + 1] = 0.0F; |
| 1088 | ret[COL_DIMLASER * 3 + 2] = 0.0F; |
| 1089 | |
| 1090 | for (i = 0; i < 3; i++) { |
| 1091 | ret[COL_GRID * 3 + i] = ret[COL_BACKGROUND * 3 + i] * 0.9F; |
| 1092 | ret[COL_LOCK * 3 + i] = ret[COL_BACKGROUND * 3 + i] * 0.7F; |
| 1093 | ret[COL_COVER * 3 + i] = ret[COL_BACKGROUND * 3 + i] * 0.5F; |
| 1094 | ret[COL_TEXT * 3 + i] = 0.0F; |
| 1095 | } |
| 1096 | |
| 1097 | ret[COL_FLASHTEXT * 3 + 0] = 0.0F; |
| 1098 | ret[COL_FLASHTEXT * 3 + 1] = 1.0F; |
| 1099 | ret[COL_FLASHTEXT * 3 + 2] = 0.0F; |
| 1100 | |
| 1101 | *ncolours = NCOLOURS; |
| 1102 | return ret; |
| 1103 | } |
| 1104 | |
| 1105 | static game_drawstate *game_new_drawstate(drawing *dr, game_state *state) |
| 1106 | { |
| 1107 | struct game_drawstate *ds = snew(struct game_drawstate); |
| 1108 | |
| 1109 | ds->tilesize = 0; |
| 1110 | ds->w = state->w; ds->h = state->h; |
| 1111 | ds->grid = snewn((state->w+2)*(state->h+2), unsigned int); |
| 1112 | memset(ds->grid, 0, (state->w+2)*(state->h+2)*sizeof(unsigned int)); |
| 1113 | ds->started = ds->reveal = 0; |
| 1114 | ds->flash_laserno = LASER_EMPTY; |
| 1115 | ds->isflash = 0; |
| 1116 | |
| 1117 | return ds; |
| 1118 | } |
| 1119 | |
| 1120 | static void game_free_drawstate(drawing *dr, game_drawstate *ds) |
| 1121 | { |
| 1122 | sfree(ds->grid); |
| 1123 | sfree(ds); |
| 1124 | } |
| 1125 | |
| 1126 | static void draw_arena_tile(drawing *dr, game_state *gs, game_drawstate *ds, |
| 1127 | int ax, int ay, int force, int isflash) |
| 1128 | { |
| 1129 | int gx = ax+1, gy = ay+1; |
| 1130 | int gs_tile = GRID(gs, gx, gy), ds_tile = GRID(ds, gx, gy); |
| 1131 | int dx = TODRAW(gx), dy = TODRAW(gy); |
| 1132 | |
| 1133 | if (gs_tile != ds_tile || gs->reveal != ds->reveal || force) { |
| 1134 | int bcol, bg; |
| 1135 | |
| 1136 | bg = (gs->reveal ? COL_BACKGROUND : |
| 1137 | (gs_tile & BALL_LOCK) ? COL_LOCK : COL_COVER); |
| 1138 | |
| 1139 | draw_rect(dr, dx, dy, TILE_SIZE, TILE_SIZE, bg); |
| 1140 | draw_rect_outline(dr, dx, dy, TILE_SIZE, TILE_SIZE, COL_GRID); |
| 1141 | |
| 1142 | if (gs->reveal) { |
| 1143 | /* Guessed balls are always black; if they're incorrect they'll |
| 1144 | * have a red cross added later. |
| 1145 | * Missing balls are red. */ |
| 1146 | if (gs_tile & BALL_GUESS) { |
| 1147 | bcol = isflash ? bg : COL_BALL; |
| 1148 | } else if (gs_tile & BALL_CORRECT) { |
| 1149 | bcol = isflash ? bg : COL_WRONG; |
| 1150 | } else { |
| 1151 | bcol = bg; |
| 1152 | } |
| 1153 | } else { |
| 1154 | /* guesses are black/black, all else background. */ |
| 1155 | if (gs_tile & BALL_GUESS) { |
| 1156 | bcol = COL_BALL; |
| 1157 | } else { |
| 1158 | bcol = bg; |
| 1159 | } |
| 1160 | } |
| 1161 | |
| 1162 | draw_circle(dr, dx + TILE_SIZE/2, dy + TILE_SIZE/2, ds->crad-1, |
| 1163 | bcol, bcol); |
| 1164 | |
| 1165 | if (gs->reveal && |
| 1166 | (gs_tile & BALL_GUESS) && |
| 1167 | !(gs_tile & BALL_CORRECT)) { |
| 1168 | int x1 = dx + 3, y1 = dy + 3; |
| 1169 | int x2 = dx + TILE_SIZE - 3, y2 = dy + TILE_SIZE-3; |
| 1170 | int coords[8]; |
| 1171 | |
| 1172 | /* Incorrect guess; draw a red cross over the ball. */ |
| 1173 | coords[0] = x1-1; |
| 1174 | coords[1] = y1+1; |
| 1175 | coords[2] = x1+1; |
| 1176 | coords[3] = y1-1; |
| 1177 | coords[4] = x2+1; |
| 1178 | coords[5] = y2-1; |
| 1179 | coords[6] = x2-1; |
| 1180 | coords[7] = y2+1; |
| 1181 | draw_polygon(dr, coords, 4, COL_WRONG, COL_WRONG); |
| 1182 | coords[0] = x2+1; |
| 1183 | coords[1] = y1+1; |
| 1184 | coords[2] = x2-1; |
| 1185 | coords[3] = y1-1; |
| 1186 | coords[4] = x1-1; |
| 1187 | coords[5] = y2-1; |
| 1188 | coords[6] = x1+1; |
| 1189 | coords[7] = y2+1; |
| 1190 | draw_polygon(dr, coords, 4, COL_WRONG, COL_WRONG); |
| 1191 | } |
| 1192 | draw_update(dr, dx, dy, TILE_SIZE, TILE_SIZE); |
| 1193 | } |
| 1194 | GRID(ds,gx,gy) = gs_tile; |
| 1195 | } |
| 1196 | |
| 1197 | static void draw_laser_tile(drawing *dr, game_state *gs, game_drawstate *ds, |
| 1198 | game_ui *ui, int lno, int force) |
| 1199 | { |
| 1200 | int gx, gy, dx, dy, unused; |
| 1201 | int wrong, omitted, reflect, hit, laserval, flash = 0, tmp; |
| 1202 | unsigned int gs_tile, ds_tile, exitno; |
| 1203 | |
| 1204 | tmp = range2grid(gs, lno, &gx, &gy, &unused); |
| 1205 | assert(tmp); |
| 1206 | gs_tile = GRID(gs, gx, gy); |
| 1207 | ds_tile = GRID(ds, gx, gy); |
| 1208 | dx = TODRAW(gx); |
| 1209 | dy = TODRAW(gy); |
| 1210 | |
| 1211 | wrong = gs->exits[lno] & LASER_WRONG; |
| 1212 | omitted = gs->exits[lno] & LASER_OMITTED; |
| 1213 | exitno = gs->exits[lno] & ~LASER_FLAGMASK; |
| 1214 | |
| 1215 | reflect = gs_tile & LASER_REFLECT; |
| 1216 | hit = gs_tile & LASER_HIT; |
| 1217 | laserval = gs_tile & ~LASER_FLAGMASK; |
| 1218 | |
| 1219 | if (lno == ui->flash_laserno) |
| 1220 | gs_tile |= LASER_FLASHED; |
| 1221 | else if (!(gs->exits[lno] & (LASER_HIT | LASER_REFLECT))) { |
| 1222 | if (exitno == ui->flash_laserno) |
| 1223 | gs_tile |= LASER_FLASHED; |
| 1224 | } |
| 1225 | if (gs_tile & LASER_FLASHED) flash = 1; |
| 1226 | |
| 1227 | gs_tile |= wrong | omitted; |
| 1228 | |
| 1229 | if (gs_tile != ds_tile || force) { |
| 1230 | draw_rect(dr, dx, dy, TILE_SIZE, TILE_SIZE, COL_BACKGROUND); |
| 1231 | draw_rect_outline(dr, dx, dy, TILE_SIZE, TILE_SIZE, COL_GRID); |
| 1232 | |
| 1233 | if (gs_tile &~ (LASER_WRONG | LASER_OMITTED)) { |
| 1234 | char str[10]; |
| 1235 | int tcol = flash ? COL_FLASHTEXT : omitted ? COL_WRONG : COL_TEXT; |
| 1236 | |
| 1237 | if (reflect || hit) |
| 1238 | sprintf(str, "%s", reflect ? "R" : "H"); |
| 1239 | else |
| 1240 | sprintf(str, "%d", laserval); |
| 1241 | |
| 1242 | if (wrong) { |
| 1243 | draw_circle(dr, dx + TILE_SIZE/2, dy + TILE_SIZE/2, |
| 1244 | ds->rrad, |
| 1245 | COL_WRONG, COL_WRONG); |
| 1246 | draw_circle(dr, dx + TILE_SIZE/2, dy + TILE_SIZE/2, |
| 1247 | ds->rrad - TILE_SIZE/16, |
| 1248 | COL_BACKGROUND, COL_WRONG); |
| 1249 | } |
| 1250 | |
| 1251 | draw_text(dr, dx + TILE_SIZE/2, dy + TILE_SIZE/2, |
| 1252 | FONT_VARIABLE, TILE_SIZE/2, ALIGN_VCENTRE | ALIGN_HCENTRE, |
| 1253 | tcol, str); |
| 1254 | } |
| 1255 | draw_update(dr, dx, dy, TILE_SIZE, TILE_SIZE); |
| 1256 | } |
| 1257 | GRID(ds, gx, gy) = gs_tile; |
| 1258 | } |
| 1259 | |
| 1260 | |
| 1261 | static void game_redraw(drawing *dr, game_drawstate *ds, game_state *oldstate, |
| 1262 | game_state *state, int dir, game_ui *ui, |
| 1263 | float animtime, float flashtime) |
| 1264 | { |
| 1265 | int i, x, y, ts = TILE_SIZE, isflash = 0, force = 0; |
| 1266 | |
| 1267 | if (flashtime > 0) { |
| 1268 | int frame = (int)(flashtime / FLASH_FRAME); |
| 1269 | isflash = (frame % 2) == 0; |
| 1270 | debug(("game_redraw: flashtime = %f", flashtime)); |
| 1271 | } |
| 1272 | |
| 1273 | if (!ds->started) { |
| 1274 | int x0 = TODRAW(0)-1, y0 = TODRAW(0)-1; |
| 1275 | int x1 = TODRAW(state->w+2), y1 = TODRAW(state->h+2); |
| 1276 | |
| 1277 | draw_rect(dr, 0, 0, |
| 1278 | TILE_SIZE * (state->w+3), TILE_SIZE * (state->h+3), |
| 1279 | COL_BACKGROUND); |
| 1280 | |
| 1281 | /* clockwise around the outline starting at pt behind (1,1). */ |
| 1282 | draw_line(dr, x0+ts, y0+ts, x0+ts, y0, COL_HIGHLIGHT); |
| 1283 | draw_line(dr, x0+ts, y0, x1-ts, y0, COL_HIGHLIGHT); |
| 1284 | draw_line(dr, x1-ts, y0, x1-ts, y0+ts, COL_LOWLIGHT); |
| 1285 | draw_line(dr, x1-ts, y0+ts, x1, y0+ts, COL_HIGHLIGHT); |
| 1286 | draw_line(dr, x1, y0+ts, x1, y1-ts, COL_LOWLIGHT); |
| 1287 | draw_line(dr, x1, y1-ts, x1-ts, y1-ts, COL_LOWLIGHT); |
| 1288 | draw_line(dr, x1-ts, y1-ts, x1-ts, y1, COL_LOWLIGHT); |
| 1289 | draw_line(dr, x1-ts, y1, x0+ts, y1, COL_LOWLIGHT); |
| 1290 | draw_line(dr, x0+ts, y1, x0+ts, y1-ts, COL_HIGHLIGHT); |
| 1291 | draw_line(dr, x0+ts, y1-ts, x0, y1-ts, COL_LOWLIGHT); |
| 1292 | draw_line(dr, x0, y1-ts, x0, y0+ts, COL_HIGHLIGHT); |
| 1293 | draw_line(dr, x0, y0+ts, x0+ts, y0+ts, COL_HIGHLIGHT); |
| 1294 | /* phew... */ |
| 1295 | |
| 1296 | draw_update(dr, 0, 0, |
| 1297 | TILE_SIZE * (state->w+3), TILE_SIZE * (state->h+3)); |
| 1298 | force = 1; |
| 1299 | ds->started = 1; |
| 1300 | } |
| 1301 | |
| 1302 | if (isflash != ds->isflash) force = 1; |
| 1303 | |
| 1304 | /* draw the arena */ |
| 1305 | for (x = 0; x < state->w; x++) { |
| 1306 | for (y = 0; y < state->h; y++) { |
| 1307 | draw_arena_tile(dr, state, ds, x, y, force, isflash); |
| 1308 | } |
| 1309 | } |
| 1310 | |
| 1311 | /* draw the lasers */ |
| 1312 | for (i = 0; i < 2*(state->w+state->h); i++) { |
| 1313 | draw_laser_tile(dr, state, ds, ui, i, force); |
| 1314 | } |
| 1315 | |
| 1316 | /* draw the 'finish' button */ |
| 1317 | if (CAN_REVEAL(state)) { |
| 1318 | clip(dr, TODRAW(0), TODRAW(0), TILE_SIZE-1, TILE_SIZE-1); |
| 1319 | draw_circle(dr, TODRAW(0) + ds->crad, TODRAW(0) + ds->crad, ds->crad, |
| 1320 | COL_BUTTON, COL_BALL); |
| 1321 | unclip(dr); |
| 1322 | } else { |
| 1323 | draw_rect(dr, TODRAW(0), TODRAW(0), |
| 1324 | TILE_SIZE-1, TILE_SIZE-1, COL_BACKGROUND); |
| 1325 | } |
| 1326 | draw_update(dr, TODRAW(0), TODRAW(0), TILE_SIZE, TILE_SIZE); |
| 1327 | ds->reveal = state->reveal; |
| 1328 | ds->flash_laserno = ui->flash_laserno; |
| 1329 | ds->isflash = isflash; |
| 1330 | |
| 1331 | { |
| 1332 | char buf[256]; |
| 1333 | |
| 1334 | if (ds->reveal) { |
| 1335 | if (state->nwrong == 0 && |
| 1336 | state->nmissed == 0 && |
| 1337 | state->nright >= state->minballs) |
| 1338 | sprintf(buf, "CORRECT!"); |
| 1339 | else |
| 1340 | sprintf(buf, "%d wrong and %d missed balls.", |
| 1341 | state->nwrong, state->nmissed); |
| 1342 | } else if (state->justwrong) { |
| 1343 | sprintf(buf, "Wrong! Guess again."); |
| 1344 | } else { |
| 1345 | if (state->nguesses > state->maxballs) |
| 1346 | sprintf(buf, "%d too many balls marked.", |
| 1347 | state->nguesses - state->maxballs); |
| 1348 | else if (state->nguesses <= state->maxballs && |
| 1349 | state->nguesses >= state->minballs) |
| 1350 | sprintf(buf, "Click button to verify guesses."); |
| 1351 | else if (state->maxballs == state->minballs) |
| 1352 | sprintf(buf, "Balls marked: %d / %d", |
| 1353 | state->nguesses, state->minballs); |
| 1354 | else |
| 1355 | sprintf(buf, "Balls marked: %d / %d-%d.", |
| 1356 | state->nguesses, state->minballs, state->maxballs); |
| 1357 | } |
| 1358 | if (ui->errors) { |
| 1359 | sprintf(buf + strlen(buf), " (%d error%s)", |
| 1360 | ui->errors, ui->errors > 1 ? "s" : ""); |
| 1361 | } |
| 1362 | status_bar(dr, buf); |
| 1363 | } |
| 1364 | } |
| 1365 | |
| 1366 | static float game_anim_length(game_state *oldstate, game_state *newstate, |
| 1367 | int dir, game_ui *ui) |
| 1368 | { |
| 1369 | return 0.0F; |
| 1370 | } |
| 1371 | |
| 1372 | static float game_flash_length(game_state *oldstate, game_state *newstate, |
| 1373 | int dir, game_ui *ui) |
| 1374 | { |
| 1375 | if (!oldstate->reveal && newstate->reveal) |
| 1376 | return 4.0F * FLASH_FRAME; |
| 1377 | else |
| 1378 | return 0.0F; |
| 1379 | } |
| 1380 | |
| 1381 | static int game_wants_statusbar(void) |
| 1382 | { |
| 1383 | return TRUE; |
| 1384 | } |
| 1385 | |
| 1386 | static int game_timing_state(game_state *state, game_ui *ui) |
| 1387 | { |
| 1388 | return TRUE; |
| 1389 | } |
| 1390 | |
| 1391 | static void game_print_size(game_params *params, float *x, float *y) |
| 1392 | { |
| 1393 | } |
| 1394 | |
| 1395 | static void game_print(drawing *dr, game_state *state, int tilesize) |
| 1396 | { |
| 1397 | } |
| 1398 | |
| 1399 | #ifdef COMBINED |
| 1400 | #define thegame blackbox |
| 1401 | #endif |
| 1402 | |
| 1403 | const struct game thegame = { |
| 1404 | "Black Box", "games.blackbox", |
| 1405 | default_params, |
| 1406 | game_fetch_preset, |
| 1407 | decode_params, |
| 1408 | encode_params, |
| 1409 | free_params, |
| 1410 | dup_params, |
| 1411 | TRUE, game_configure, custom_params, |
| 1412 | validate_params, |
| 1413 | new_game_desc, |
| 1414 | validate_desc, |
| 1415 | new_game, |
| 1416 | dup_game, |
| 1417 | free_game, |
| 1418 | TRUE, solve_game, |
| 1419 | FALSE, game_text_format, |
| 1420 | new_ui, |
| 1421 | free_ui, |
| 1422 | encode_ui, |
| 1423 | decode_ui, |
| 1424 | game_changed_state, |
| 1425 | interpret_move, |
| 1426 | execute_move, |
| 1427 | PREFERRED_TILE_SIZE, game_compute_size, game_set_size, |
| 1428 | game_colours, |
| 1429 | game_new_drawstate, |
| 1430 | game_free_drawstate, |
| 1431 | game_redraw, |
| 1432 | game_anim_length, |
| 1433 | game_flash_length, |
| 1434 | FALSE, FALSE, game_print_size, game_print, |
| 1435 | game_wants_statusbar, |
| 1436 | FALSE, game_timing_state, |
| 1437 | 0, /* mouse_priorities */ |
| 1438 | }; |
| 1439 | |
| 1440 | /* vim: set shiftwidth=4 tabstop=8: */ |