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