1/* 2 * Copyright (C) 2010-2013 Robert Ancell 3 * 4 * This program is free software: you can redistribute it and/or modify it under 5 * the terms of the GNU General Public License as published by the Free Software 6 * Foundation, either version 2 of the License, or (at your option) any later 7 * version. See http://www.gnu.org/copyleft/gpl.html the full text of the 8 * license. 9 */ 10 11/** 12 * This is the model layer of a tile. 13 */ 14private class Tile : Object 15{ 16 /* Property */ 17 private bool _closed = false; 18 internal bool closed 19 { 20 internal get { return _closed; } 21 private set 22 { 23 _closed = value; 24 /* Send close signal */ 25 if (_closed) 26 close (grid_x, grid_y); 27 } 28 } 29 30 public uint8 grid_x { internal get; protected construct set; } 31 public uint8 grid_y { internal get; protected construct set; } 32 public uint8 color { internal get; protected construct; } /* 1 <= color <= 4 */ 33 // Vala tip or bug: looks like "private" means "accessible only from this file"; but if both get and set are private, there is a warning 34 internal bool visited { protected get; private set; default = false; } 35 36 /* Signals */ 37 internal signal void move (uint8 old_x, uint8 old_y, uint8 new_x, uint8 new_y); 38 internal signal void close (uint8 grid_x, uint8 grid_y); 39 40 /* Constructor */ 41 internal Tile (uint8 x, uint8 y, uint8 c) 42 { 43 Object (grid_x: x, grid_y: y, color: c); 44 } 45 46 /* Do not use this mothod to initialize the position. */ 47 internal void update_position (uint8 new_x, uint8 new_y) 48 { 49 if (closed) 50 return; 51 52 uint8 old_x = grid_x; 53 uint8 old_y = grid_y; 54 55 if ((new_x != old_x) || (new_y != old_y)) 56 { 57 grid_x = new_x; 58 grid_y = new_y; 59 60 /* Send move signal to actor in the view */ 61 if (!closed) 62 move (old_x, old_y, new_x, new_y); 63 } 64 } 65} 66 67/** 68 * This is the model layer of the whole game. All game logic goes here. This class tries not to 69 * bring in any visual stuff to comply the separation of view-model idea. 70 */ 71private class Game : Object 72{ 73 private Tile? [,] current_board; 74 private bool is_started = false; 75 76 /* Game score */ 77 internal uint score { internal get; private set; default = 0; } 78 79 private uint8 _color_num = 3; 80 public uint8 color_num 81 { 82 internal get { return _color_num; } 83 protected construct set // TODO should be doable to make construct only again 84 { 85 if (value < 2 || value > 4) 86 _color_num = 3; 87 else 88 _color_num = value; 89 } 90 } 91 92 /* Property */ 93 public uint8 rows { internal get; protected construct; } 94 public uint8 columns { internal get; protected construct; } 95 96 internal signal void update_score (uint points_awarded); 97 internal signal void complete (); 98 internal signal void started (); 99 100 /* Constructor */ 101 internal Game (uint8 rows, uint8 columns, uint8 color_num, Variant? saved_game) 102 { 103 Object (rows: rows, columns: columns, color_num: color_num); 104 if (saved_game == null || !load_saved_game ((!) saved_game)) 105 create_new_game (); 106 } 107 108// private static string to_string (ref Tile [,] current_board) 109// { 110// uint8 rows = (uint8) current_board.length [0]; 111// uint8 columns = (uint8) current_board.length [1]; 112// string board = "\n"; 113// for (uint8 row = rows; row > 0; row--) 114// { 115// for (uint8 col = 0; col < columns; col++) 116// if (current_board [row - 1, col] == null) 117// board += ". "; 118// else if (((!) current_board [row - 1, col]).closed) 119// board += "0 "; 120// else 121// board += ((!) current_board [row - 1, col]).color.to_string () + " "; 122// board += "\n"; 123// } 124// return (board); 125// } 126 127 private inline void create_new_game () 128 { 129 initial_board = new uint8 [rows, columns]; 130 current_board = new Tile? [rows, columns]; 131 132 /* populate with the requested number of colors */ 133 do (populate_new_game (ref initial_board, color_num)); 134 while (bad_colors_number (ref initial_board, color_num) 135 || unclickable_board (ref initial_board)); 136 137 /* create the board of Tile instances */ 138 for (uint8 x = 0; x < columns; x++) 139 for (uint8 y = 0; y < rows; y++) 140 current_board [y, x] = new Tile (x, y, initial_board [y, x]); 141 142 is_started = false; 143 } 144 private static void populate_new_game (ref uint8 [,] initial_board, uint8 color_num) 145 { 146 uint8 rows = (uint8) initial_board.length [0]; 147 uint8 columns = (uint8) initial_board.length [1]; 148 for (uint8 x = 0; x < columns; x++) 149 for (uint8 y = 0; y < rows; y++) 150 initial_board [y, x] = (uint8) Math.floor (Random.next_double () * color_num) + 1; 151 } 152 private static bool bad_colors_number (ref uint8 [,] initial_board, uint8 color_num) 153 { 154 /* counter will grow to twice the number of colors */ 155 uint8 counter = 0; 156 uint8 [] colors = new uint8 [color_num]; 157 for (uint8 x = 0; x < color_num; x++) 158 colors [x] = 0; 159 160 uint8 rows = (uint8) initial_board.length [0]; 161 uint8 columns = (uint8) initial_board.length [1]; 162 for (uint8 x = 0; x < columns; x++) 163 for (uint8 y = 0; y < rows; y++) 164 { 165 uint8 color_id = initial_board [y, x]; 166 /* initial board should be full */ 167 if (color_id == 0) 168 assert_not_reached (); 169 color_id--; 170 /* color number too big for given number of colors */ 171 if (color_id >= color_num) 172 return true; 173 /* already (at least) two tiles of this color */ 174 if (colors [color_id] >= 2) 175 continue; 176 /* check if board is now completely good */ 177 counter++; 178 if (counter >= 2 * color_num) 179 return false; 180 /* else just increase the per-color counter */ 181 colors [color_id]++; 182 } 183 return true; 184 } 185 private static bool unclickable_board (ref uint8 [,] initial_board) 186 { 187 uint8 rows = (uint8) initial_board.length [0]; 188 uint8 columns = (uint8) initial_board.length [1]; 189 for (uint8 x = 1; x < columns; x++) 190 for (uint8 y = 0; y < rows; y++) 191 if (initial_board [y, x] == initial_board [y, x - 1]) 192 return false; 193 for (uint8 x = 0; x < columns; x++) 194 for (uint8 y = 1; y < rows; y++) 195 if (initial_board [y, x] == initial_board [y - 1, x]) 196 return false; 197 return true; 198 } 199 200 /* Recursively find all the connected tile from given_tile */ 201 private static List<Tile> _connected_tiles_real (Tile? given_tile, ref Tile? [,] current_board) 202 { 203 List<Tile> cl = new List<Tile> (); 204 205 if (given_tile == null || ((!) given_tile).visited || ((!) given_tile).closed) 206 return cl; 207 208 uint8 x = ((!) given_tile).grid_x; 209 uint8 y = ((!) given_tile).grid_y; 210 211 ((!) given_tile).visited = true; 212 213 cl.append ((!) given_tile); 214 215 unowned Tile? tile = current_board [y + 1, x]; 216 if (y + 1 < current_board.length [0] 217 && tile != null && (((!) given_tile).color == ((!) tile).color)) 218 cl.concat (_connected_tiles_real (tile, ref current_board)); 219 220 if (y >= 1) 221 { 222 tile = current_board [y - 1, x]; 223 if (tile != null && (((!) given_tile).color == ((!) tile).color)) 224 cl.concat (_connected_tiles_real (tile, ref current_board)); 225 } 226 227 tile = current_board [y, x + 1]; 228 if (x + 1 < current_board.length [1] 229 && tile != null && (((!) given_tile).color == ((!) tile).color)) 230 cl.concat (_connected_tiles_real (tile, ref current_board)); 231 232 if (x >= 1) 233 { 234 tile = current_board [y, x - 1]; 235 if (tile != null && (((!) given_tile).color == ((!) tile).color)) 236 cl.concat (_connected_tiles_real (tile, ref current_board)); 237 } 238 239 return cl; 240 } 241 242 internal List<Tile> connected_tiles (Tile given_tile) 243 { 244 return _connected_tiles (given_tile, ref current_board); 245 } 246 private static List<Tile> _connected_tiles (Tile given_tile, ref Tile? [,] current_board) 247 { 248 List<Tile> cl = _connected_tiles_real (given_tile, ref current_board); 249 250 foreach (unowned Tile? tile in current_board) 251 { 252 if (tile != null) 253 ((!) tile).visited = false; 254 } 255 256 /* single tile will be ignored */ 257 if (cl.length () < 2) 258 return new List<Tile> (); // technically similar to null, but clearer from Vala pov 259 260 return cl; 261 } 262 263 internal Tile? get_tile (uint8 x, uint8 y) 264 { 265 return current_board [y, x]; 266 } 267 268 internal void remove_connected_tiles (Tile given_tile) 269 { 270 remove_connected_tiles_real (given_tile, /* skip history */ false); 271 } 272 273 private void remove_connected_tiles_real (Tile given_tile, bool skip_history) 274 { 275 _remove_connected_tiles (given_tile, ref current_board, skip_history); 276 277 if (!is_started) { 278 is_started = true; 279 started (); 280 } 281 282 if (has_completed (ref current_board)) 283 { 284 if (has_won (ref current_board)) 285 increment_score (1000); 286 complete (); 287 } 288 } 289 private void _remove_connected_tiles (Tile given_tile, ref Tile? [,] current_board, bool skip_history) 290 { 291 List<Tile> cl = _connected_tiles (given_tile, ref current_board); 292 293 if (cl.length () < 2) 294 return; 295 296 foreach (unowned Tile tile in (!) cl) 297 tile.closed = true; 298 299 uint8 new_x = 0; 300 uint8 [] removed_columns = {}; 301 302 for (uint8 x = 0; x < columns; x++) 303 { 304 List<Tile> not_closed_tiles = new List<Tile> (); 305 List<Tile> closed_tiles = new List<Tile> (); 306 307 /* for each column, separate not-closed and closed tiles */ 308 for (uint8 y = 0; y < rows; y++) 309 { 310 unowned Tile? tile = current_board [y, x]; 311 312 if (tile == null) 313 break; 314 315 if (((!) tile).closed) 316 closed_tiles.append ((!) tile); 317 else 318 not_closed_tiles.append ((!) tile); 319 } 320 321 /* append closed tiles to not-closed tiles */ 322 not_closed_tiles.concat ((owned) closed_tiles); 323 324 /* update tile array at the current column, not-closed tiles are at the bottom, closed ones top */ 325 for (uint8 y = 0; y < rows; y++) 326 current_board [y, new_x] = not_closed_tiles.nth_data (y); 327 328 /* flag to check if current column is empty */ 329 bool has_empty_col = true; 330 331 /* update the positions (grid_x, grid_y) of tiles at the current column */ 332 for (uint8 y = 0; y < rows; y++) 333 { 334 unowned Tile? tile = current_board [y, new_x]; 335 336 if (tile == null) 337 break; 338 339 if (!((!) tile).closed) 340 { 341 ((!) tile).update_position (new_x, y); 342 has_empty_col = false; 343 } 344 } 345 346 /* If the current column is empty, don't increment new_x. Otherwise increment */ 347 if (!has_empty_col) 348 new_x++; 349 else 350 { 351 int length = removed_columns.length; 352 removed_columns.resize (length + 1); 353 removed_columns.move (/* start */ 0, /* dest */ 1, /* length */ length); 354 removed_columns [0] = new_x; 355 } 356 } 357 358 /* The remaining columns are do-not-cares. Assign null to them */ 359 for (; new_x < columns; new_x++) 360 for (uint8 y = 0; y < rows; y++) 361 current_board [y, new_x] = null; 362 363 increment_score_from_tiles ((uint16) cl.length ()); 364 365 if (!skip_history) 366 add_history_entry (given_tile.grid_x, given_tile.grid_y, given_tile.color, cl, (owned) removed_columns); 367 } 368 369 private static bool has_completed (ref Tile? [,] current_board) 370 { 371 foreach (unowned Tile? tile in current_board) 372 { 373 if (tile != null && !((!) tile).closed && (_connected_tiles ((!) tile, ref current_board).length () > 1)) 374 return false; 375 } 376 377 return true; 378 } 379 380 private static bool has_won (ref Tile? [,] current_board) 381 { 382 foreach (unowned Tile? tile in current_board) 383 { 384 if (tile != null && !((!) tile).closed) 385 return false; 386 } 387 388 return true; 389 } 390 391 private inline void decrement_score_from_tiles (uint16 n_tiles) 392 { 393 increment_score (-1 * get_score_from_tiles (n_tiles)); 394 } 395 396 private inline void increment_score_from_tiles (uint16 n_tiles) 397 { 398 increment_score (get_score_from_tiles (n_tiles)); 399 } 400 401 private inline int get_score_from_tiles (uint16 n_tiles) 402 { 403 return n_tiles < 3 ? 0 : (n_tiles - 2) * (n_tiles - 2); 404 } 405 406 private void increment_score (int variation) 407 { 408 score += variation; 409 if (variation > 0) 410 update_score (variation); 411 else 412 update_score (0); 413 } 414 415 /*\ 416 * * loading and saving 417 \*/ 418 419 private uint8 [,] initial_board; 420 421 private inline bool load_saved_game (Variant variant) 422 { 423 if (variant.get_type_string () != "m(aayqa(yy))") 424 return false; // assert_not_reached() ? 425 426 Variant? child = variant.get_maybe (); 427 if (child == null) 428 return false; 429 430 VariantIter iter = new VariantIter ((!) child); 431 Variant? board_variant = iter.next_value (); 432 if (board_variant == null) 433 assert_not_reached (); 434 uint16 history_index; 435 iter.next ("q", out history_index); 436 Variant? history_variant = iter.next_value (); 437 if (history_variant == null) 438 assert_not_reached (); 439 440 // all the following way to extract values feels horrible, but there is a bug when trying to do it properly (05/2020) 441 Variant? tmp_variant_1 = ((!) board_variant).get_child_value (0); 442 if (tmp_variant_1 == null) 443 return false; 444 uint rows = (uint) ((!) board_variant).n_children (); 445 uint columns = (uint) ((!) tmp_variant_1).n_children (); 446 if (rows != this.rows 447 || columns != this.columns) 448 return false; 449 450 uint8 [,] initial_board = new uint8 [rows, columns]; 451 for (uint8 i = 0; i < rows; i++) 452 { 453 tmp_variant_1 = ((!) board_variant).get_child_value (i); 454 for (uint8 j = 0; j < columns; j++) 455 { 456 Variant tmp_variant_2 = tmp_variant_1.get_child_value (j); 457 uint8 color = tmp_variant_2.get_byte (); 458 if (color > 4) 459 return false; 460 if (color == 0) 461 return false; 462 initial_board [rows - i - 1, j] = color; 463 } 464 } 465 466 if (bad_colors_number (ref initial_board, color_num) 467 || unclickable_board (ref initial_board)) 468 return false; 469 470 Tile? [,] current_board = new Tile? [rows, columns]; 471 for (uint8 i = 0; i < rows; i++) 472 for (uint8 j = 0; j < columns; j++) 473 current_board [i, j] = new Tile (j, i, initial_board [i, j]); 474 475 iter = new VariantIter ((!) history_variant); 476 while (iter != null) 477 { 478 tmp_variant_1 = iter.next_value (); 479 if (tmp_variant_1 == null) 480 break; 481 _remove_connected_tiles (current_board [rows - tmp_variant_1.get_child_value (1).get_byte () - 1, 482 tmp_variant_1.get_child_value (0).get_byte ()], 483 ref current_board, 484 /* skip history */ false); 485 } 486 487 if (history_index > reversed_history.length ()) 488 { 489 clear_history (); 490 return false; 491 } 492 493 for (uint16 i = history_index; i != 0; i--) 494 undo_real (ref current_board); 495 496 if (has_completed (ref current_board)) 497 { 498 clear_history (); 499 return false; 500 } 501 502 this.current_board = current_board; 503 this.initial_board = initial_board; 504 this.history_index = history_index; 505 506 update_score (score); 507 is_started = true; 508 return true; 509 } 510 511 internal Variant get_saved_game () 512 { 513 if (!is_started || has_completed (ref current_board)) 514 return new Variant ("m(aayqa(yy))", null); 515 516 VariantBuilder builder = new VariantBuilder (new VariantType ("(aayqa(yy))")); 517 builder.open (new VariantType ("aay")); 518 VariantType ay_type = new VariantType ("ay"); 519 for (uint8 i = rows; i > 0; i--) 520 { 521 builder.open (ay_type); 522 for (uint8 j = 0; j < columns; j++) 523 builder.add ("y", initial_board [i - 1, j]); 524 builder.close (); 525 } 526 builder.close (); 527 builder.add ("q", history_index); 528 builder.open (new VariantType ("a(yy)")); 529 reversed_history.reverse (); 530 reversed_history.@foreach ((data) => { 531 if (data == null) 532 return; 533 builder.open (new VariantType ("(yy)")); 534 builder.add ("y", ((!) data).click.x); 535 builder.add ("y", rows - ((!) data).click.y - 1); 536 builder.close (); 537 }); 538 reversed_history.reverse (); // get_saved_game might be called once or twice… so let’s put reversed_history back in its (inverted) order 539 builder.close (); 540 return new Variant.maybe (/* guess the type */ null, builder.end ()); 541 } 542 543 /*\ 544 * * history 545 \*/ 546 547 [CCode (notify = true)] internal bool can_undo { internal get; private set; default = false; } 548 [CCode (notify = true)] internal bool can_redo { internal get; private set; default = false; } 549 private uint16 history_length = 0; 550 private uint16 history_index = 0; 551 552 private List<HistoryEntry> reversed_history = new List<HistoryEntry> (); 553 554 internal signal void undone (); 555 556 private class Point : Object 557 { 558 public uint8 x { internal get; protected construct; } 559 public uint8 y { internal get; protected construct; } 560 561 internal Point (uint8 x, uint8 y) 562 { 563 Object (x: x, y: y); 564 } 565 } 566 567 private class HistoryEntry : Object 568 { 569 [CCode (notify = false)] public Point click { internal get; protected construct; } 570 [CCode (notify = false)] public uint8 color { internal get; protected construct; } 571 572 internal List<Point> removed_tiles = new List<Point> (); 573 internal uint8 [] removed_columns; 574 575 internal HistoryEntry (uint8 x, uint8 y, uint8 color, List<Tile> cl, owned uint8 [] removed_columns) 576 { 577 Object (click: new Point (x, y), color: color); 578 this.removed_columns = removed_columns; 579 580 // TODO init at construct 581 foreach (unowned Tile tile in cl) 582 removed_tiles.prepend (new Point (tile.grid_x, tile.grid_y)); 583 removed_tiles.sort ((tile_1, tile_2) => { 584 if (tile_1.x < tile_2.x) 585 return -1; 586 if (tile_1.x > tile_2.x) 587 return 1; 588 if (tile_1.y < tile_2.y) 589 return -1; 590 if (tile_1.y > tile_2.y) 591 return 1; 592 assert_not_reached (); 593 }); 594 } 595 } 596 597 private inline void clear_history () 598 { 599 reversed_history = new List<HistoryEntry> (); 600 history_length = 0; 601 history_index = 0; 602 can_undo = false; 603 can_redo = false; 604 } 605 606 private inline void add_history_entry (uint8 x, uint8 y, uint8 color, List<Tile> cl, owned uint8 [] removed_columns) 607 { 608 while (history_index > 0) 609 { 610 unowned HistoryEntry? history_data = reversed_history.nth_data (0); 611 if (history_data == null) assert_not_reached (); 612 613 reversed_history.remove ((!) history_data); 614 615 history_index--; 616 history_length--; 617 } 618 619 reversed_history.prepend (new HistoryEntry (x, y, color, cl, (owned) removed_columns)); 620 history_length++; 621 can_undo = true; 622 can_redo = false; 623 } 624 625 internal void undo () 626 { 627 undo_real (ref current_board); 628 } 629 630 private void undo_real (ref Tile? [,] current_board) 631 { 632 if (!can_undo) 633 return; 634 635 unowned List<HistoryEntry>? history_item = reversed_history.nth (history_index); 636 if (history_item == null) assert_not_reached (); 637 638 unowned HistoryEntry? history_data = ((!) history_item).data; 639 if (history_data == null) assert_not_reached (); 640 641 undo_move ((!) history_data, ref current_board); 642 643 if (history_index == history_length) 644 can_undo = false; 645 can_redo = true; 646 } 647 private inline void undo_move (HistoryEntry history_entry, ref Tile? [,] current_board) 648 { 649 if (has_won (ref current_board)) 650 increment_score (-1000); 651 decrement_score_from_tiles ((uint16) history_entry.removed_tiles.length ()); 652 653 foreach (uint8 removed_column in history_entry.removed_columns) 654 { 655 for (uint8 j = columns - 1; j > removed_column; j--) 656 { 657 for (uint8 i = 0; i < rows; i++) 658 { 659 if (current_board [i, j - 1] != null) 660 { 661 current_board [i, j] = (owned) current_board [i, j - 1]; 662 ((!) current_board [i, j]).update_position (j, i); 663 } 664 else 665 current_board [i, j] = null; 666 } 667 } 668 for (uint8 i = 0; i < rows; i++) 669 current_board [i, removed_column] = null; 670 } 671 672 foreach (unowned Point removed_tile in history_entry.removed_tiles) 673 { 674 uint8 column = removed_tile.x; 675 for (uint8 row = rows - 1; row > removed_tile.y; row--) 676 { 677 if (current_board [row - 1, column] != null) 678 { 679 current_board [row, column] = (owned) current_board [row - 1, column]; 680 ((!) current_board [row, column]).update_position (column, row); 681 } 682 else 683 current_board [row, column] = null; 684 if (row == 0) 685 break; 686 } 687 current_board [removed_tile.y, column] = new Tile (column, removed_tile.y, history_entry.color); 688 } 689 690 history_index++; 691 692 undone (); 693 } 694 695 internal void redo () 696 { 697 if (!can_redo) 698 return; 699 700 unowned List<HistoryEntry>? history_item = reversed_history.nth (history_index - 1); 701 if (history_item == null) assert_not_reached (); 702 703 unowned HistoryEntry? history_data = ((!) history_item).data; 704 if (history_data == null) assert_not_reached (); 705 706 redo_move ((!) history_data); 707 708 if (history_index == 0) 709 can_redo = false; 710 can_undo = true; 711 } 712 private inline void redo_move (HistoryEntry history_entry) 713 { 714 history_index--; 715 716 // TODO save for real where the user clicked; warning, history_entry.click does not use the same coords system 717 remove_connected_tiles_real (current_board [history_entry.removed_tiles.first ().data.y, 718 history_entry.removed_tiles.first ().data.x], 719 /* skip history */ true); 720 } 721} 722