[four-in-a-row/KaKnife/four-in-a-row-vala: 27/65] begin transition to OOP



commit 17a345989c6d62e8dd44fb07290790b2c92808af
Author: Jacob Humphrey <jacob ryan humphrey gmail com>
Date:   Sun Dec 9 21:37:40 2018 -0600

    begin transition to OOP

 src/Makefile.am     |    2 +
 src/game_board.vala |  133 +++++
 src/gfx.vala        |  127 ++++-
 src/main.vala       | 1472 +++++++++++++++++++++------------------------------
 src/prefs.vala      |  114 ++--
 src/scorebox.vala   |   92 ++++
 6 files changed, 1001 insertions(+), 939 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index f03e842..162e120 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -9,6 +9,8 @@ four_in_a_row_SOURCES = \
                prefs.vala \
                gfx.vala \
                theme.vala \
+               scorebox.vala \
+               game_board.vala \
                config.vapi
 
 four_in_a_row_CPPFLAGS = \
diff --git a/src/game_board.vala b/src/game_board.vala
new file mode 100644
index 0000000..03bf4e9
--- /dev/null
+++ b/src/game_board.vala
@@ -0,0 +1,133 @@
+/* -*- Mode: vala; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* game_board.vala
+ *
+ * Copyright © 2018 Jacob Humphrey
+ *
+ * This file is part of GNOME Four-in-a-row.
+ *
+ * GNOME Four-in-a-row is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GNOME Four-in-a-row is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with GNOME Four-in-a-row. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class Board : Object {
+    static Tile[,] gboard;
+    const int BOARD_SIZE = 7;
+    static Once<Board> _instance;
+    public static Board instance {
+        get {
+            return _instance.once(() => {return new Board();});
+        }
+    }
+
+    public Board() {
+        gboard = new Tile[BOARD_SIZE, BOARD_SIZE];
+    }
+
+    public void @set(int x, int y, Tile tile) {
+        gboard[x,y] = tile;
+    }
+
+    public Tile @get(int x, int y) {
+        return gboard[x, y];
+    }
+
+    public void clear() {
+        for (var r = 0; r < BOARD_SIZE; r++) {
+            for (var c = 0; c < BOARD_SIZE; c++) {
+                gboard[r, c] = Tile.CLEAR;
+            }
+        }
+    }
+
+    public int first_empty_row(int c) {
+        int r = 1;
+
+        while (r < BOARD_SIZE && gboard[r, c] == Tile.CLEAR)
+            r++;
+        return r - 1;
+    }
+
+    bool is_hline_at(Tile p, int r, int c, out int r1, out int c1, out int r2, out int c2) {
+        r1 = r;
+        r2 = r;
+        c1 = c;
+        c2 = c;
+        while (c1 > 0 && gboard[r, c1 - 1] == p)
+            c1 = c1 - 1;
+        while (c2 < 6 && gboard[r, c2 + 1] == p)
+            c2 = c2 + 1;
+        if (c2 - c1 >= 3)
+            return true;
+        return false;
+    }
+
+    public bool is_line_at(Tile p, int r, int c, out int r1 = null,
+                           out int c1 = null, out int r2 = null, out int c2 = null) {
+        return is_hline_at(p, r, c, out r1, out c1, out r2, out c2) ||
+            is_vline_at(p, r, c, out r1, out c1, out r2, out c2) ||
+            is_dline1_at(p, r, c, out r1, out c1, out r2, out c2) ||
+            is_dline2_at(p, r, c, out r1, out c1, out r2, out c2);
+    }
+
+    bool is_vline_at(Tile p, int r, int c, out int r1 , out int c1, out int r2, out int c2) {
+        r1 = r;
+        r2 = r;
+        c1 = c;
+        c2 = c;
+        while (r1 > 1 && get(r1 - 1, c) == p)
+            r1 = r1 - 1;
+        while (r2 < 6 && get(r2 + 1, c) == p)
+            r2 = r2 + 1;
+        if (r2 - r1 >= 3)
+            return true;
+        return false;
+    }
+
+    bool is_dline1_at(Tile p, int r, int c, out int r1, out int c1, out int r2, out int c2) {
+        /* upper left to lower right */
+        r1 = r;
+        r2 = r;
+        c1 = c;
+        c2 = c;
+        while (c1 > 0 && r1 > 1 && get(r1 - 1, c1 - 1) == p) {
+            r1 = r1 - 1;
+            c1 = c1 - 1;
+        }
+        while (c2 < 6 && r2 < 6 && get(r2 + 1, c2 + 1) == p) {
+            r2 = r2 + 1;
+            c2 = c2 + 1;
+        }
+        if (r2 - r1 >= 3)
+            return true;
+        return false;
+    }
+
+    bool is_dline2_at(Tile p, int r, int c, out int r1, out int c1, out int r2, out int c2) {
+        /* upper right to lower left */
+        r1 = r;
+        r2 = r;
+        c1 = c;
+        c2 = c;
+        while (c1 < 6 && r1 > 1 && get(r1 - 1, c1 + 1) == p) {
+            r1 = r1 - 1;
+            c1 = c1 + 1;
+        }
+        while (c2 > 0 && r2 < 6 && get(r2 + 1, c2 - 1) == p) {
+            r2 = r2 + 1;
+            c2 = c2 - 1;
+        }
+        if (r2 - r1 >= 3)
+            return true;
+        return false;
+    }
+}
diff --git a/src/gfx.vala b/src/gfx.vala
index 18c8a14..de53a02 100644
--- a/src/gfx.vala
+++ b/src/gfx.vala
@@ -19,22 +19,44 @@
  * along with GNOME Four-in-a-row. If not, see <http://www.gnu.org/licenses/>.
  */
 
-Gtk.Widget drawarea;
-int[,] gboard;
+
 int boardsize = 0;
 int tilesize = 0;
 int offset[6];
 
-namespace Gfx {
+class GameBoardView : Gtk.DrawingArea {
     /* unscaled pixbufs */
     Gdk.Pixbuf pb_tileset_raw;
     Gdk.Pixbuf pb_bground_raw;
-
     /* scaled pixbufs */
     Gdk.Pixbuf pb_tileset;
     Gdk.Pixbuf pb_bground;
+    //public Gtk.DrawingArea drawarea;
+
+    static Once<GameBoardView> _instance;
+    public static GameBoardView instance {
+        get {
+            return _instance.once(() => {return new GameBoardView();});
+        }
+    }
 
-    int get_column(int xpos) {
+    public GameBoardView() {
+        Object();
+        /* set a min size to avoid pathological behavior of gtk when scaling down */
+        set_size_request(350, 350);
+        halign = Gtk.Align.FILL;
+        valign = Gtk.Align.FILL;
+
+        events = Gdk.EventMask.EXPOSURE_MASK |
+                          Gdk.EventMask.BUTTON_PRESS_MASK |
+                          Gdk.EventMask.BUTTON_RELEASE_MASK;
+        configure_event.connect(resize);
+        draw.connect(expose);
+        //button_press_event.connect(button_press_event);
+        key_press_event.connect(this.on_key_press);
+    }
+
+    public int get_column(int xpos) {
         /* Derive column from pixel position */
         int c = xpos / tilesize;
         if (c > 6)
@@ -45,28 +67,19 @@ namespace Gfx {
         return c;
     }
 
-    void draw_tile(int r, int c) {
-        drawarea.queue_draw_area(c*tilesize, r*tilesize, tilesize, tilesize);
+    public void draw_tile(int r, int c) {
+        queue_draw_area(c*tilesize, r*tilesize, tilesize, tilesize);
     }
 
-    void draw_all() {
-        drawarea.queue_draw_area(0, 0, boardsize, boardsize);
-    }
-
-    bool change_theme() {
-        if (!Gfx.load_pixmaps())
-            return false;
-
-        Gfx.refresh_pixmaps();
-        draw_all();
-        return true;
+    public void draw_all() {
+        queue_draw_area(0, 0, boardsize, boardsize);
     }
 
-    void resize(Gtk.Widget w) {
+    public bool resize(Gdk.EventConfigure e) {
         int width, height;
 
-        width = w.get_allocated_width();
-        height = w.get_allocated_height();
+        width = get_allocated_width();
+        height = get_allocated_height();
 
         boardsize = int.min(width, height);
         tilesize = boardsize / 7;
@@ -78,11 +91,21 @@ namespace Gfx {
         offset[Tile.PLAYER1_CURSOR] = tilesize * 4;
         offset[Tile.PLAYER2_CURSOR] = tilesize * 5;
 
-        Gfx.refresh_pixmaps();
+        refresh_pixmaps();
         draw_all();
+        return true;
+    }
+
+     public bool change_theme() {
+        if (!load_pixmaps())
+            return false;
+
+        refresh_pixmaps();
+        GameBoardView.instance.draw_all();
+        return true;
     }
 
-    void expose(Cairo.Context cr) {
+    public bool expose(Cairo.Context cr) {
         int r, c;
 
         /* draw the background */
@@ -94,11 +117,12 @@ namespace Gfx {
 
         for (r = 0; r < 7; r++) {
             for (c = 0; c < 7; c++) {
-                Gfx.paint_tile(cr, r, c);
+                paint_tile(cr, r, c);
             }
         }
 
         draw_grid(cr);
+        return false;
     }
 
     void draw_grid(Cairo.Context cr) {
@@ -145,7 +169,7 @@ namespace Gfx {
     void paint_tile(Cairo.Context cr, int r, int c) {
         int x = c * tilesize;
         int y = r * tilesize;
-        int tile = gboard[r,c];
+        int tile = Board.instance.get(r, c);
         int os = 0;
 
         if (tile == Tile.CLEAR && r != 0)
@@ -181,13 +205,13 @@ namespace Gfx {
         cr.restore();
     }
 
-    void refresh_pixmaps() {
+    public void refresh_pixmaps() {
         /* scale the pixbufs */
         pb_tileset = pb_tileset_raw.scale_simple(tilesize * 6, tilesize, Gdk.InterpType.BILINEAR);
         pb_bground = pb_bground_raw.scale_simple(boardsize, boardsize, Gdk.InterpType.BILINEAR);
     }
 
-    bool load_pixmaps() {
+    public bool load_pixmaps() {
         string fname;
         Gdk.Pixbuf pb_tileset_tmp;
         Gdk.Pixbuf pb_bground_tmp = null;
@@ -202,7 +226,7 @@ namespace Gfx {
                     p.theme_id = 0;
                     continue;
                 } else {
-                    Gfx.load_error(fname);
+                    load_error(fname);
                     return false;
                 }
             }
@@ -216,7 +240,7 @@ namespace Gfx {
             try {
                 pb_bground_tmp = new Gdk.Pixbuf.from_file(fname);
             } catch (Error e) {
-                Gfx.load_error(fname);
+                load_error(fname);
                 return false;
             }
         }
@@ -250,4 +274,49 @@ namespace Gfx {
 
         return true;
     }
+
+    protected override bool button_press_event(Gdk.EventButton e) {
+        int x, y;
+        if (application.player_active) {
+            return false;
+        }
+
+        if (application.gameover && timeout == 0) {
+            application.blink_winner(2);
+        } else if (application.is_player_human() && timeout == 0) {
+            get_window().get_device_position(e.device, out x, out y, null);
+            application.game_process_move(GameBoardView.instance.get_column(x));
+        }
+
+        return true;
+    }
+
+    bool on_key_press(Gtk.Widget  w, Gdk.EventKey  e) {
+        if ((application.player_active) || timeout != 0 ||
+                (e.keyval != p.keypress[Move.LEFT] &&
+                e.keyval != p.keypress[Move.RIGHT] &&
+                e.keyval != p.keypress[Move.DROP])) {
+            return false;
+        }
+
+        if (application.gameover) {
+            application.blink_winner(2);
+            return true;
+        }
+
+        if (e.keyval == p.keypress[Move.LEFT] && column != 0) {
+            column_moveto--;
+            application.move_cursor(column_moveto);
+        } else if (e.keyval == p.keypress[Move.RIGHT] && column < 6) {
+            column_moveto++;
+            application.move_cursor(column_moveto);
+        } else if (e.keyval == p.keypress[Move.DROP]) {
+            application.game_process_move(column);
+        }
+        return true;
+    }
+
+
 }
+
+
diff --git a/src/main.vala b/src/main.vala
index d774acb..facace6 100644
--- a/src/main.vala
+++ b/src/main.vala
@@ -19,6 +19,15 @@
  * along with GNOME Four-in-a-row. If not, see <http://www.gnu.org/licenses/>.
  */
 
+const string APPNAME_LONG = N_("Four-in-a-row");
+const int SIZE_VSTR = 53;
+const int SPEED_BLINK = 150;
+const int SPEED_MOVE = 35;
+const int SPEED_DROP = 20;
+const char vlevel[] = {'0','a','b','c','\0'};
+const int DEFAULT_WIDTH = 495;
+const int DEFAULT_HEIGHT = 435;
+
 public enum AnimID {
     NONE,
     MOVE,
@@ -28,7 +37,7 @@ public enum AnimID {
 }
 
 public enum PlayerID {
-    PLAYER1,
+    PLAYER1 = 0,
     PLAYER2,
     NOBODY
 }
@@ -41,7 +50,7 @@ public enum Level {
 }
 
 public enum Tile {
-    PLAYER1,
+    PLAYER1 = 0,
     PLAYER2,
     CLEAR,
     CLEAR_CURSOR,
@@ -64,1010 +73,757 @@ public enum SoundID {
     COLUMN_FULL
 }
 
-SimpleAction hint_action;
-SimpleAction undo_action;
-SimpleAction new_game_action;
-
-const string APPNAME_LONG = N_("Four-in-a-row");
-const int SIZE_VSTR = 53;
-
-Gtk.Application? application;
-Gtk.Window window;
-Gtk.Dialog? scorebox = null;
-Gtk.Label label_name[3];
-Gtk.Label label_score[3];
-bool gameover;
-bool player_active;
-PlayerID player;
-PlayerID winner;
-PlayerID who_starts;
-int score[3];
-AnimID anim;
-char vstr[53];
-const char vlevel[] = {'0','a','b','c','\0'};
-int moves;
-const int SPEED_BLINK = 150;
-const int SPEED_MOVE = 35;
-const int SPEED_DROP = 20;
-int column;
-int column_moveto;
-int row;
-int row_dropto;
-Gtk.HeaderBar headerbar;
-
-const int DEFAULT_WIDTH = 495;
-const int DEFAULT_HEIGHT = 435;
-
-int blink_r1 = 0;
-int blink_c1 = 0;
-int blink_r2 = 0;
-int blink_c2 = 0;
-int blink_t = 0;
-int blink_n = 0;
-bool blink_on = false;
-uint timeout = 0;
+class FourInARow : Gtk.Application {
+    public bool gameover;
+    public bool player_active;
 
-void on_game_new(SimpleAction a, Variant? v) {
-    stop_anim();
-    game_reset();
-}
+    const ActionEntry app_entries[] = {
+        {"scores", on_game_scores},
+        {"quit", on_game_exit},
+        {"preferences", on_settings_preferences},
+        {"help", on_help_contents},
+        {"about", on_help_about}
+    };
 
-void draw_line(int r1, int c1, int r2, int c2, int tile) {
-    /* draw a line of 'tile' from r1,c1 to r2,c2 */
-
-    bool done = false;
-    int d_row = 0;
-    int d_col = 0;
-
-    if (r1 < r2)
-        d_row = 1;
-    else if (r1 > r2)
-        d_row = -1;
-
-    if (c1 < c2)
-        d_col = 1;
-    else if (c1 > c2)
-        d_col = -1;
-
-    do {
-        done = (r1 == r2 && c1 == c2);
-        gboard[r1, c1] = tile;
-        Gfx.draw_tile(r1, c1);
-        if (r1 != r2)
-            r1 += d_row;
-        if (c1 != c2)
-            c1 += d_col;
-    } while (!done);
-}
+    public void game_reset() {
+        stop_anim();
 
-public int main(string[] argv) {
-    gboard = new int[7,7];
-    Intl.setlocale();
+        undo_action.set_enabled(false);
+        hint_action.set_enabled(false);
 
-    application = new Gtk.Application("org.gnome.four-in-a-row", 0);
+        who_starts = (who_starts == PlayerID.PLAYER1)
+            ? PlayerID.PLAYER2 : PlayerID.PLAYER1;
+        player = who_starts;
 
-    Intl.bindtextdomain(Config.GETTEXT_PACKAGE, Config.LOCALEDIR);
-    Intl.bind_textdomain_codeset(Config.GETTEXT_PACKAGE, "UTF-8");
-    Intl.textdomain(Config.GETTEXT_PACKAGE);
+        gameover = true;
+        player_active = false;
+        winner = NOBODY;
+        column = 3;
+        column_moveto = 3;
+        row = 0;
+        row_dropto = 0;
 
-    application.startup.connect(create_app);
-    application.activate.connect(activate);
+        clear_board();
+        set_status_message(null);
+        GameBoardView.instance.draw_all();
 
-    var context = new OptionContext();
-    context.add_group(Gtk.get_option_group(true));
-    try {
-        context.parse(ref argv);
-    } catch (Error error) {
-        print("%s", error.message);
-        return 1;
+        move_cursor(column);
+        gameover = false;
+        prompt_player();
+        if (!is_player_human()) {
+            vstr[0] = player == PLAYER1 ? vlevel[p.level[PlayerID.PLAYER1]]
+                : vlevel[p.level[PlayerID.PLAYER2]];
+            game_process_move(playgame((string)vstr) - 1);
+        }
     }
 
-    settings = new GLib.Settings("org.gnome.four-in-a-row");
+    public void blink_winner(int n) {
+        /* blink the winner's line(s) n times */
 
-    Environment.set_application_name(_(Config.APPNAME_LONG));
+        if (winner == NOBODY)
+            return;
 
-    prefs_init();
-    game_init();
+        blink_t = winner;
+
+        if (Board.instance.is_line_at((Tile)winner, row, column, out blink_r1,
+                                      out blink_c1, out blink_r2, out blink_c2)) {
+            anim = AnimID.BLINK;
+            blink_on = false;
+            blink_n = n;
+            var temp = new Animate(0);
+            timeout = Timeout.add(SPEED_BLINK,  temp.exec);
+            while (timeout!=0)
+                Gtk.main_iteration();
+        }
 
-    if (!Gfx.load_pixmaps())
-        return 1;
+    }
 
-    var app_retval = application.run(argv);
+    public void add_actions() {
+        new_game_action = new SimpleAction("new-game", null);
+        new_game_action.activate.connect(this.on_game_new);
+        add_action(new_game_action);
 
-    return app_retval;
-}
+        hint_action = new SimpleAction("hint", null);
+        hint_action.activate.connect(this.on_game_hint);
+        add_action(hint_action);
 
-public void activate() {
-    if (!window.is_visible()) {
-        window.show_all();
-        Gfx.refresh_pixmaps();
-        Gfx.draw_all();
-        scorebox_update();       /* update visible player descriptions */
-        prompt_player();
-        game_reset();
-    }
-}
+        undo_action = new SimpleAction("undo-move", null);
+        undo_action.activate.connect(on_game_undo);
+        add_action(undo_action);
 
-class NextMove {
-    int c;
+        set_accels_for_action("app.new-game", {"<Primary>n"});
+        set_accels_for_action("app.hint", {"<Primary>h"});
+        set_accels_for_action("app.undo-move", {"<Primary>z"});
+        set_accels_for_action("app.quit", {"<Primary>q"});
+        set_accels_for_action("app.contents", {"F1"});
 
-    public NextMove(int c) {
-        this.c = c;
+        add_action_entries(app_entries, this);
     }
 
-    public bool exec() {
-        return next_move(c);
+    void on_game_new(Variant? v) {
+        stop_anim();
+        game_reset();
     }
-}
 
-public bool next_move(int c) {
-    process_move(c);
-    return false;
-}
+    public void draw_line(int r1, int c1, int r2, int c2, int tile) {
+        /* draw a line of 'tile' from r1,c1 to r2,c2 */
 
-public void game_process_move(int c) {
-    process_move(c);
-}
+        bool done = false;
+        int d_row = 0;
+        int d_col = 0;
 
-public void game_init() {
-    anim = AnimID.NONE;
-    gameover = true;
-    player_active = false;
-    player = PlayerID.PLAYER1;
-    winner = PlayerID.NOBODY;
-    score[PlayerID.PLAYER1] = 0;
-    score[PlayerID.PLAYER2] = 0;
-    score[PlayerID.NOBODY] = 0;
+        if (r1 < r2)
+            d_row = 1;
+        else if (r1 > r2)
+            d_row = -1;
 
-    who_starts = PlayerID.PLAYER2;     /* This gets reversed immediately. */
-
-    clear_board();
-}
+        if (c1 < c2)
+            d_col = 1;
+        else if (c1 > c2)
+            d_col = -1;
 
-void clear_board() {
-    int r, c, i;
+        do {
+            done = (r1 == r2 && c1 == c2);
+            Board.instance.set(r1, c1, (Tile) tile);
+            GameBoardView.instance.draw_tile(r1, c1);
+            if (r1 != r2)
+                r1 += d_row;
+            if (c1 != c2)
+                c1 += d_col;
+        } while (!done);
+    }
 
-    for (r = 0; r < 7; r++) {
-        for (c = 0; c < 7; c++) {
-            gboard[r, c] = Tile.CLEAR;
+    public FourInARow() {
+        Object(application_id: "org.gnome.four-in-a-row",
+               flags: ApplicationFlags.FLAGS_NONE);
+        anim = AnimID.NONE;
+        gameover = true;
+        player_active = false;
+        player = PlayerID.PLAYER1;
+        winner = PlayerID.NOBODY;
+        score[PlayerID.PLAYER1] = 0;
+        score[PlayerID.PLAYER2] = 0;
+        score[PlayerID.NOBODY] = 0;
+
+        who_starts = PlayerID.PLAYER2;     /* This gets reversed immediately. */
+
+        clear_board();
+    }
+
+    protected override void activate() {
+        if (!window.is_visible()) {
+            window.show_all();
+            GameBoardView.instance.refresh_pixmaps();
+            GameBoardView.instance.draw_all();
+            scorebox.update();       /* update visible player descriptions */
+            prompt_player();
+            game_reset();
         }
     }
 
-    for (i = 0; i < SIZE_VSTR; i++)
-        vstr[i] = '\0';
-
-    vstr[0] = vlevel[Level.WEAK];
-    vstr[1] = '0';
-    moves = 0;
-}
-
-int first_empty_row(int c) {
-    int r = 1;
+    // protected override void startup() {
+    //     create_app(this);
+    // }
 
-    while (r < 7 && gboard[r, c] == Tile.CLEAR)
-        r++;
-    return r - 1;
-}
-
-int get_n_human_players() {
-    if (p.level[PlayerID.PLAYER1] != Level.HUMAN && p.level[PlayerID.PLAYER2] != Level.HUMAN)
-        return 0;
-    if (p.level[PlayerID.PLAYER1] != Level.HUMAN || p.level[PlayerID.PLAYER2] != Level.HUMAN)
-        return 1;
-    return 2;
-}
-
-bool is_player_human() {
-    return player == PLAYER1 ? p.level[PlayerID.PLAYER1] == Level.HUMAN
-        : p.level[PlayerID.PLAYER2] == Level.HUMAN;
-}
+    public void prompt_player() {
+        int players = p.get_n_human_players();
+        bool human = is_player_human();
+        string who;
+        string str;
 
-static void drop_marble(int r, int c) {
-    int tile;
-    tile = player == PlayerID.PLAYER1 ? Tile.PLAYER1 : Tile.PLAYER2;
+        hint_action.set_enabled(human && !gameover);
 
-    gboard[r, c] = tile;
-    Gfx.draw_tile(r, c);
+        switch (players) {
+        case 0:
+            undo_action.set_enabled(false);
+            break;
+        case 1:
+            undo_action.set_enabled((human && moves >1) || (!human && gameover));
+            break;
+        case 2:
+            undo_action.set_enabled(moves > 0);
+            break;
+        }
 
-    column = column_moveto = c;
-    row = row_dropto = r;
-}
+        if (gameover && winner == PlayerID.NOBODY) {
+            if (score[PlayerID.NOBODY] == 0)
+                set_status_message(null);
+            else
+                set_status_message(_("It’s a draw!"));
+            return;
+        }
 
-void drop() {
-    Tile tile = player == PLAYER1 ? Tile.PLAYER1 : Tile.PLAYER2;
+        switch (players) {
+        case 1:
+            if (human) {
+                if (gameover)
+                    set_status_message(_("You win!"));
+                else
+                    set_status_message(_("Your Turn"));
+            } else {
+                if (gameover)
+                    set_status_message(_("I win!"));
+                else
+                    set_status_message(_("I’m Thinking…"));
+            }
+            break;
+        case 2:
+        case 0:
 
-    gboard[row, column] = Tile.CLEAR;
-    Gfx.draw_tile(row, column);
+            if (gameover) {
+                who = player == PLAYER1 ? theme_get_player_win(PlayerID.PLAYER1)
+                    : theme_get_player_win(PlayerID.PLAYER2);
+                str =  _(who);
+            } else if (player_active) {
+                set_status_message(_("Your Turn"));
+                return;
+            } else {
+                who = player == PLAYER1 ? theme_get_player_turn(PlayerID.PLAYER1)
+                    : theme_get_player_turn(PlayerID.PLAYER2);
+                str =  _(who);
+            }
 
-    row++;
-    gboard[row, column] = tile;
-    Gfx.draw_tile(row, column);
-}
+            set_status_message(str);
+            break;
+        }
+    }
 
-void move(int c) {
-    gboard[0, column] = Tile.CLEAR;
-    Gfx.draw_tile(0, column);
+    public void swap_player() {
+        player = (player == PlayerID.PLAYER1) ? PlayerID.PLAYER2 : PlayerID.PLAYER1;
+        move_cursor(3);
+        prompt_player();
+    }
 
-    column = c;
-    gboard[0, c] = player == PlayerID.PLAYER1 ? Tile.PLAYER1 : Tile.PLAYER2;
+    public void game_process_move(int c) {
+        process_move(c);
+    }
 
-    Gfx.draw_tile(0, c);
-}
+    public void play_sound(SoundID id) {
+        string name;
 
-static void move_cursor(int c) {
-    move(c);
-    column = column_moveto = c;
-    row = row_dropto = 0;
-}
+        if (!p.do_sound)
+            return;
 
-void swap_player() {
-    player = (player == PlayerID.PLAYER1) ? PlayerID.PLAYER2 : PlayerID.PLAYER1;
-    move_cursor(3);
-    prompt_player();
-}
+        switch (id) {
+        case SoundID.DROP:
+            name = "slide";
+            break;
+        case SoundID.I_WIN:
+            name = "reverse";
+            break;
+        case SoundID.YOU_WIN:
+            name = "bonus";
+            break;
+        case SoundID.PLAYER_WIN:
+            name = "bonus";
+            break;
+        case SoundID.DRAWN_GAME:
+            name = "reverse";
+            break;
+        case SoundID.COLUMN_FULL:
+            name = "bad";
+            break;
+        default:
+            return;
+        }
 
-void set_status_message(string? message) {
-    headerbar.set_title(message);
-}
+        string filename, path;
 
-static void blink_tile(int r, int c, int t, int n) {
-    if (timeout != 0)
-        return;
-    blink_r1 = r;
-    blink_c1 = c;
-    blink_r2 = r;
-    blink_c2 = c;
-    blink_t = t;
-    blink_n = n;
-    blink_on = false;
-    anim = AnimID.BLINK;
-    var temp = new Animate(0);
-    timeout = Timeout.add(SPEED_BLINK, temp.exec);
-}
+        filename = name + ".ogg";
+        path = Path.build_filename(Config.SOUND_DIRECTORY, filename);
 
-void stop_anim() {
-    if (timeout == 0)
-        return;
-    anim = AnimID.NONE;
-    Source.remove(timeout);
-    timeout = 0;
-}
+        CanberraGtk.context_get().play(
+                id,
+                Canberra.PROP_MEDIA_NAME, name,
+                Canberra.PROP_MEDIA_FILENAME, path);
 
-bool on_drawarea_resize(Gtk.Widget w, Gdk.EventConfigure e) {
-    Gfx.resize(w);
-    return true;
-}
+    }
 
-bool on_drawarea_draw(Gtk.Widget w, Cairo.Context cr) {
-    Gfx.expose(cr);
-    return false;
-}
+    public void process_move3(int c) {
+        play_sound(SoundID.DROP);
 
-void on_game_undo(SimpleAction action, Variant? parameter) {
-    int r, c;
+        vstr[++moves] = '1' + (char)c;
+        vstr[moves + 1] = '0';
 
-    if (timeout != 0)
-        return;
-    c = vstr[moves] - '0' - 1;
-    r = first_empty_row(c) + 1;
-    vstr[moves] = '0';
-    vstr[moves + 1] = '\0';
-    moves--;
+        check_game_state();
 
-    if (gameover) {
-        score[winner]--;
-        scorebox_update();
-        gameover = false;
-        prompt_player();
-    } else {
-        swap_player();
-    }
-    move_cursor(c);
-
-    gboard[r, c] = Tile.CLEAR;
-    Gfx.draw_tile(r, c);
-
-    if (get_n_human_players() == 1 && !is_player_human()) {
-        if (moves > 0) {
-            c = vstr[moves] - '0' - 1;
-            r = first_empty_row(c) + 1;
-            vstr[moves] = '0';
-            vstr[moves + 1] = '\0';
-            moves--;
+        if (gameover) {
+            score[winner]++;
+            scorebox.update();
+            prompt_player();
+        } else {
             swap_player();
-            move_cursor(c);
-            gboard[r, c] = Tile.CLEAR;
-            Gfx.draw_tile(r, c);
+            if (!is_player_human()) {
+                vstr[0] = player == PlayerID.PLAYER1 ? vlevel[p.level[PlayerID.PLAYER1]]
+                    : vlevel[p.level[PlayerID.PLAYER2]];
+                c = playgame((string)vstr) - 1;
+                if (c < 0)
+                    gameover = true;
+                var nm = new NextMove(c);
+                Timeout.add(SPEED_DROP, nm.exec);
+            }
         }
     }
-}
-void on_game_scores(SimpleAction action, Variant? parameter) {
-    Gtk.Grid grid, grid2;
-    Gtk.Widget icon;
 
-    if (scorebox != null) {
-        scorebox.present();
-        return;
-    }
+    public void game_init() {
+        anim = AnimID.NONE;
+        gameover = true;
+        player_active = false;
+        player = PlayerID.PLAYER1;
+        winner = PlayerID.NOBODY;
+        score[PlayerID.PLAYER1] = 0;
+        score[PlayerID.PLAYER2] = 0;
+        score[PlayerID.NOBODY] = 0;
 
-    scorebox = new Gtk.Dialog.with_buttons(_("Scores"), window,
-    Gtk.DialogFlags.DESTROY_WITH_PARENT | Gtk.DialogFlags.USE_HEADER_BAR);
+        who_starts = PlayerID.PLAYER2;     /* This gets reversed immediately. */
 
-    scorebox.set_resizable(false);
-    scorebox.set_border_width(5);
-    scorebox.get_content_area().set_spacing(2);
+        clear_board();
+    }
 
-    grid = new Gtk.Grid();
-    grid.set_halign(Gtk.Align.CENTER);
-    grid.set_row_spacing(6);
-    grid.set_orientation(Gtk.Orientation.VERTICAL);
-    grid.set_border_width(5);
+    public bool is_player_human() {
+        return player == PLAYER1 ? p.level[PlayerID.PLAYER1] == Level.HUMAN
+            : p.level[PlayerID.PLAYER2] == Level.HUMAN;
+    }
 
-    scorebox.get_content_area().pack_start(grid);
+    public void process_move2(int c) {
+        int r = Board.instance.first_empty_row(c);
+        if (r > 0) {
+            row = 0;
+            row_dropto = r;
+            anim = AnimID.DROP;
+            var temp = new Animate(c);
+            timeout = Timeout.add(SPEED_DROP, temp.exec);
+        } else {
+            application.play_sound(SoundID.COLUMN_FULL);
+        }
+    }
 
-    grid2 = new Gtk.Grid();
-    grid.add(grid2);
-    grid2.set_column_spacing(6);
+    public void process_move(int c) {
+        if (timeout != 0) {
+            var temp = new Animate(c);
+            Timeout.add(SPEED_DROP, temp.exec);
+            return;
+        }
 
-    label_name[PlayerID.PLAYER1] = new Gtk.Label(null);
-    grid2.attach(label_name[PlayerID.PLAYER1], 0, 0, 1, 1);
-    label_name[PlayerID.PLAYER1].set_xalign(0);
-    label_name[PlayerID.PLAYER1].set_yalign(0.5f);
+        column_moveto = c;
+        anim = AnimID.MOVE;
+        var temp = new Animate(c);
+        timeout = Timeout.add(SPEED_DROP, temp.exec);
+    }
 
-    label_score[PlayerID.PLAYER1] = new Gtk.Label(null);
-    grid2.attach(label_score[PlayerID.PLAYER1], 1, 0, 1, 1);
-    label_score[PlayerID.PLAYER1].set_xalign(0);
-    label_score[PlayerID.PLAYER1].set_yalign(0.5f);
+    void drop_marble(int r, int c) {
+        Tile tile = player == PlayerID.PLAYER1 ? Tile.PLAYER1 : Tile.PLAYER2;
 
-    label_name[PlayerID.PLAYER2] = new Gtk.Label(null);
-    grid2.attach(label_name[PlayerID.PLAYER2], 0, 1, 1, 1);
-    label_name[PlayerID.PLAYER2].set_xalign(0);
-    label_name[PlayerID.PLAYER2].set_yalign(0.5f);
+        Board.instance.set(r, c, tile);
+        GameBoardView.instance.draw_tile(r, c);
 
-    label_score[PlayerID.PLAYER2] = new Gtk.Label(null);
-    grid2.attach(label_score[PlayerID.PLAYER2], 1, 0, 1, 1);
-    label_score[PlayerID.PLAYER2].set_xalign(0);
-    label_score[PlayerID.PLAYER2].set_yalign(0.5f);
+        column = column_moveto = c;
+        row = row_dropto = r;
+    }
 
-    label_name[PlayerID.NOBODY] = new Gtk.Label(_("Drawn:"));
-    grid2.attach(label_name[PlayerID.NOBODY], 0, 2, 1, 1);
-    label_name[PlayerID.NOBODY].set_xalign(0);
-    label_name[PlayerID.NOBODY].set_yalign(0.5f);
+    public void drop() {
+        Tile tile = player == PLAYER1 ? Tile.PLAYER1 : Tile.PLAYER2;
 
-    label_score[PlayerID.NOBODY] = new Gtk.Label(null);
-    grid2.attach(label_score[PlayerID.NOBODY], 1, 0, 1, 1);
-    label_score[PlayerID.NOBODY].set_xalign(0);
-    label_score[PlayerID.NOBODY].set_yalign(0.5f);
+        Board.instance.set(row, column, Tile.CLEAR);
+        GameBoardView.instance.draw_tile(row, column);
 
-    scorebox.show_all();
+        row++;
+        Board.instance.set(row, column, tile);
+        GameBoardView.instance.draw_tile(row, column);
+    }
 
-    scorebox_update();
-}
+    public void move(int c) {
+        Board.instance.set(0, column, Tile.CLEAR);
+        GameBoardView.instance.draw_tile(0, column);
 
-void on_game_exit(SimpleAction action, Variant? parameter) {
-    stop_anim();
-    application.quit();
-}
+        column = c;
+        Board.instance.set(0, c, player == PlayerID.PLAYER1 ? Tile.PLAYER1 : Tile.PLAYER2);
 
-void process_move2(int c) {
-    int r = first_empty_row(c);
-    if (r > 0) {
-        row = 0;
-        row_dropto = r;
-        anim = AnimID.DROP;
-        var temp = new Animate(c);
-        timeout = Timeout.add(SPEED_DROP, temp.exec);
-    } else {
-        play_sound(SoundID.COLUMN_FULL);
+        GameBoardView.instance.draw_tile(0, c);
     }
-}
 
-bool is_vline_at(PlayerID p, int r, int c, int * r1, int * c1, int * r2, int * c2) {
-    *r1 = *r2 = r;
-    *c1 = *c2 = c;
-    while (*r1 > 1 && gboard[*r1 - 1, c] == p)
-        *r1 = *r1 - 1;
-    while (*r2 < 6 && gboard[*r2 + 1, c] == p)
-        *r2 = *r2 + 1;
-    if (*r2 - *r1 >= 3)
-        return true;
-    return false;
-}
-
-bool is_dline1_at(PlayerID p, int r, int c, int * r1, int * c1, int * r2, int * c2) {
-    /* upper left to lower right */
-    *r1 = *r2 = r;
-    *c1 = *c2 = c;
-    while (*c1 > 0 && *r1 > 1 && gboard[*r1 - 1, *c1 - 1] == p) {
-        *r1 = *r1 - 1;
-        *c1 = *c1 - 1;
-    }
-    while (*c2 < 6 && *r2 < 6 && gboard[*r2 + 1, *c2 + 1] == p) {
-        *r2 = *r2 + 1;
-        *c2 = *c2 + 1;
+    public void move_cursor(int c) {
+        move(c);
+        column = column_moveto = c;
+        row = row_dropto = 0;
     }
-    if (*r2 - *r1 >= 3)
-        return true;
-    return false;
-}
-
-bool is_line_at(PlayerID p, int r, int c) {
-    int r1, r2, c1, c2;
-
-    return is_hline_at(p, r, c, &r1, &c1, &r2, &c2) ||
-        is_vline_at(p, r, c, &r1, &c1, &r2, &c2) ||
-        is_dline1_at(p, r, c, &r1, &c1, &r2, &c2) ||
-        is_dline2_at(p, r, c, &r1, &c1, &r2, &c2);
-}
 
-bool is_dline2_at(PlayerID p, int r, int c, int * r1, int * c1, int * r2, int * c2) {
-    /* upper right to lower left */
-    *r1 = *r2 = r;
-    *c1 = *c2 = c;
-    while (*c1 < 6 && *r1 > 1 && gboard[*r1 - 1, *c1 + 1] == p) {
-        *r1 = *r1 - 1;
-        *c1 = *c1 + 1;
-    }
-    while (*c2 > 0 && *r2 < 6 && gboard[*r2 + 1, *c2 - 1] == p) {
-        *r2 = *r2 + 1;
-        *c2 = *c2 - 1;
+    void set_status_message(string? message) {
+        headerbar.set_title(message);
     }
-    if (*r2 - *r1 >= 3)
-        return true;
-    return false;
-}
 
-bool is_hline_at(PlayerID p, int r, int c, int * r1, int * c1, int * r2, int * c2) {
-    *r1 = *r2 = r;
-    *c1 = *c2 = c;
-    while (*c1 > 0 && gboard[r, *c1 - 1] == p)
-        *c1 = *c1 - 1;
-    while (*c2 < 6 && gboard[r, *c2 + 1] == p)
-        *c2 = *c2 + 1;
-    if (*c2 - *c1 >= 3)
-        return true;
-    return false;
-}
+    class NextMove {
+        int c;
 
-void scorebox_reset() {
-    score[PlayerID.PLAYER1] = 0;
-    score[PlayerID.PLAYER2] = 0;
-    score[PlayerID.NOBODY] = 0;
-    scorebox_update();
-}
+        public NextMove(int c) {
+            this.c = c;
+        }
 
-void process_move(int c) {
-    if (timeout != 0) {
-        var temp = new Animate(c);
-        Timeout.add(SPEED_DROP, temp.exec);
-        return;
+        public bool exec() {
+            application.process_move(c);
+            return false;
+        }
     }
 
-    column_moveto = c;
-    anim = AnimID.MOVE;
-    var temp = new Animate(c);
-    timeout = Timeout.add(SPEED_DROP, temp.exec);
-}
+    void stop_anim() {
+        if (timeout == 0)
+            return;
+        anim = AnimID.NONE;
+        Source.remove(timeout);
+        timeout = 0;
+    }
 
-void on_help_about(SimpleAction action, Variant? parameter) {
-    const string authors[] = {"Tim Musson <trmusson ihug co nz>",
-        "David Neary <bolsh gimp org>",
-        "Nikhar Agrawal <nikharagrawal2006 gmail com>",
-        "Jacob Humphrey <jacob ryan humphrey gmail com"
-    };
+    void clear_board() {
+        Board.instance.clear();
 
-    const string artists[] = { "Alan Horkan",
-        "Anatol Drlicek",
-        "Based on the Faenza icon theme by Matthieu James"
-    };
+        for (var i = 0; i < SIZE_VSTR; i++)
+            vstr[i] = '\0';
 
-    const string documenters[] = {"Timothy Musson"};
-
-    Gtk.show_about_dialog(window,
-        name: _(Config.APPNAME_LONG),
-        version: Config.VERSION,
-        copyright: "Copyright © 1999–2008 Tim Musson and David Neary\nCopyright © 2014 Michael 
Catanzaro\nCopyright © 2018 Jacob Humphrey",
-        license_type: Gtk.License.GPL_2_0,
-        comments: _("Connect four in a row to win"),
-        authors: authors,
-        documenters: documenters,
-        artists: artists,
-        translator_credits: _("translator-credits"),
-        logo_icon_name: "four-in-a-row",
-        website: "https://wiki.gnome.org/Apps/Four-in-a-row";);
-}
-
-void check_game_state() {
-    if (is_line_at(player, row, column)) {
-        gameover = true;
-        winner = player;
-        switch (get_n_human_players()) {
-        case 1:
-            play_sound(is_player_human() ? SoundID.YOU_WIN : SoundID.I_WIN);
-            break;
-        case 0:
-        case 2:
-            play_sound(SoundID.PLAYER_WIN);
-            break;
-        }
-        blink_winner(6);
-    } else if (moves == 42) {
-        gameover = true;
-        winner = NOBODY;
-        play_sound(SoundID.DRAWN_GAME);
+        vstr[0] = vlevel[Level.WEAK];
+        vstr[1] = '0';
+        moves = 0;
     }
-}
 
-void on_help_contents(SimpleAction action, Variant? parameter) {
-    try {
-        Gtk.show_uri(window.get_screen(),
-            "help:four-in-a-row",
-            Gtk.get_current_event_time());
-    } catch(Error error) {
-        warning("Failed to show help: %s", error.message);
+    void blink_tile(int r, int c, int t, int n) {
+        if (timeout != 0)
+            return;
+        blink_r1 = r;
+        blink_c1 = c;
+        blink_r2 = r;
+        blink_c2 = c;
+        blink_t = t;
+        blink_n = n;
+        blink_on = false;
+        anim = AnimID.BLINK;
+        var temp = new Animate(0);
+        timeout = Timeout.add(SPEED_BLINK, temp.exec);
     }
-}
 
-void process_move3(int c) {
-    play_sound(SoundID.DROP);
+    void on_game_hint(SimpleAction action, Variant? parameter) {
+        string s;
+        int c;
 
-    vstr[++moves] = '1' + (char)c;
-    vstr[moves + 1] = '0';
+        if (timeout != 0)
+            return;
+        if (gameover)
+            return;
 
-    check_game_state();
+        hint_action.set_enabled(false);
+        undo_action.set_enabled(false);
 
-    if (gameover) {
-        score[winner]++;
-        scorebox_update();
-        prompt_player();
-    } else {
-        swap_player();
-        if (!is_player_human()) {
-            vstr[0] = player == PlayerID.PLAYER1 ? vlevel[p.level[PlayerID.PLAYER1]]
-                : vlevel[p.level[PlayerID.PLAYER2]];
-            c = playgame((string)vstr) - 1;
-            if (c < 0)
-                gameover = true;
-            var nm = new NextMove(c);
-            Timeout.add(SPEED_DROP, nm.exec);
-        }
-    }
-}
+        application.set_status_message(_("I’m Thinking…"));
 
-void game_reset() {
-    stop_anim();
-
-    undo_action.set_enabled(false);
-    hint_action.set_enabled(false);
-
-    who_starts = (who_starts == PlayerID.PLAYER1)
-        ? PlayerID.PLAYER2 : PlayerID.PLAYER1;
-    player = who_starts;
-
-    gameover = true;
-    player_active = false;
-    winner = NOBODY;
-    column = 3;
-    column_moveto = 3;
-    row = 0;
-    row_dropto = 0;
-
-    clear_board();
-    set_status_message(null);
-    Gfx.draw_all();
-
-    move_cursor(column);
-    gameover = false;
-    prompt_player();
-    if (!is_player_human()) {
-        vstr[0] = player == PLAYER1 ? vlevel[p.level[PlayerID.PLAYER1]]
-            : vlevel[p.level[PlayerID.PLAYER2]];
-        game_process_move(playgame((string)vstr) - 1);
-    }
-}
+        vstr[0] = vlevel[Level.STRONG];
+        c = playgame((string)vstr) - 1;
 
-void play_sound(SoundID id) {
-    string name;
-
-    if (!p.do_sound)
-        return;
-
-    switch (id) {
-    case SoundID.DROP:
-        name = "slide";
-        break;
-    case SoundID.I_WIN:
-        name = "reverse";
-        break;
-    case SoundID.YOU_WIN:
-        name = "bonus";
-        break;
-    case SoundID.PLAYER_WIN:
-        name = "bonus";
-        break;
-    case SoundID.DRAWN_GAME:
-        name = "reverse";
-        break;
-    case SoundID.COLUMN_FULL:
-        name = "bad";
-        break;
-    default:
-        return;
-    }
+        column_moveto = c;
+        while (timeout != 0)
+            Gtk.main_iteration();
+        anim = AnimID.HINT;
+        var temp = new Animate(0);
+        timeout = Timeout.add(SPEED_MOVE, temp.exec);
 
-    string filename, path;
+        application.blink_tile(0, c, Board.instance.get(0, c), 6);
 
-    filename = name + ".ogg";
-    path = Path.build_filename(Config.SOUND_DIRECTORY, filename);
+        s = _("Hint: Column ")+ (c + 1).to_string();
+        application.set_status_message(s);
 
-    CanberraGtk.context_get().play(
-            id,
-            Canberra.PROP_MEDIA_NAME, name,
-            Canberra.PROP_MEDIA_FILENAME, path);
+        if (moves <= 0 || (moves == 1 && application.is_player_human()))
+            undo_action.set_enabled(false);
+        else
+            undo_action.set_enabled(true);
+    }
 
-}
+    void on_game_scores(SimpleAction action, Variant? parameter) {
+        if (scorebox != null) {
+            scorebox.present();
+            return;
+        }
 
-class Animate {
-    int c;
-    public Animate(int c) {
-        this.c = c;
+        scorebox = new Scorebox();
+        scorebox.show_all();
+        scorebox.update();
     }
 
-    public bool exec() {
-        return on_animate(c);
+    void on_game_exit(SimpleAction action, Variant? parameter) {
+        stop_anim();
+        quit();
     }
-}
 
-bool on_animate(int c = 0) {
-    if (anim == AnimID.NONE)
-        return false;
-
-    switch (anim) {
-    case AnimID.NONE:
-        break;
-    case AnimID.HINT:
-    case AnimID.MOVE:
-        if (column < column_moveto) {
-            move(column + 1);
-        } else if (column > column_moveto) {
-            move(column - 1);
-        } else {
-            timeout = 0;
-            if (anim == AnimID.MOVE) {
-                anim = AnimID.NONE;
-                process_move2(c);
-            } else {
-                anim = AnimID.NONE;
-            }
-            return false;
+    class Animate {
+        int c;
+        public Animate(int c) {
+            this.c = c;
         }
-        break;
-    case AnimID.DROP:
-        if (row < row_dropto) {
-            drop();
-        } else {
-            anim = AnimID.NONE;
-            timeout = 0;
-            process_move3(c);
-            return false;
-        }
-        break;
-    case AnimID.BLINK:
-        draw_line(blink_r1, blink_c1, blink_r2, blink_c2, blink_on ? blink_t
-            : Tile.CLEAR);
-        blink_n--;
-        if (blink_n <= 0 && blink_on) {
-            anim = AnimID.NONE;
-            timeout = 0;
+
+        public bool exec() {
+            if (anim == AnimID.NONE)
             return false;
+
+            switch (anim) {
+            case AnimID.NONE:
+                break;
+            case AnimID.HINT:
+            case AnimID.MOVE:
+                if (column < column_moveto) {
+                    application.move(column + 1);
+                } else if (column > column_moveto) {
+                    application.move(column - 1);
+                } else {
+                    timeout = 0;
+                    if (anim == AnimID.MOVE) {
+                        anim = AnimID.NONE;
+                        application.process_move2(c);
+                    } else {
+                        anim = AnimID.NONE;
+                    }
+                    return false;
+                }
+                break;
+            case AnimID.DROP:
+                if (row < row_dropto) {
+                    application.drop();
+                } else {
+                    anim = AnimID.NONE;
+                    timeout = 0;
+                    application.process_move3(c);
+                    return false;
+                }
+                break;
+            case AnimID.BLINK:
+                application.draw_line(blink_r1, blink_c1, blink_r2, blink_c2, blink_on ? blink_t
+                    : Tile.CLEAR);
+                blink_n--;
+                if (blink_n <= 0 && blink_on) {
+                    anim = AnimID.NONE;
+                    timeout = 0;
+                    return false;
+                }
+                blink_on = !blink_on;
+                break;
+            }
+            return true;
         }
-        blink_on = !blink_on;
-        break;
     }
-    return true;
-}
 
-bool on_key_press(Gtk.Widget  w, Gdk.EventKey  e) {
-    if ((player_active) || timeout != 0 ||
-            (e.keyval != p.keypress[Move.LEFT] &&
-            e.keyval != p.keypress[Move.RIGHT] &&
-            e.keyval != p.keypress[Move.DROP])) {
-        return false;
-    }
+    void on_game_undo(SimpleAction action, Variant? parameter) {
+        int r, c;
 
-    if (gameover) {
-        blink_winner(2);
-        return true;
-    }
+        if (timeout != 0)
+            return;
+        c = vstr[moves] - '0' - 1;
+        r = Board.instance.first_empty_row(c) + 1;
+        vstr[moves] = '0';
+        vstr[moves + 1] = '\0';
+        moves--;
 
-    if (e.keyval == p.keypress[Move.LEFT] && column != 0) {
-        column_moveto--;
-        move_cursor(column_moveto);
-    } else if (e.keyval == p.keypress[Move.RIGHT] && column < 6) {
-        column_moveto++;
-        move_cursor(column_moveto);
-    } else if (e.keyval == p.keypress[Move.DROP]) {
-        game_process_move(column);
+        if (gameover) {
+            score[winner]--;
+            scorebox.update();
+            gameover = false;
+            prompt_player();
+        } else {
+            swap_player();
+        }
+        move_cursor(c);
+
+        Board.instance.set(r, c, Tile.CLEAR);
+        GameBoardView.instance.draw_tile(r, c);
+
+        if (p.get_n_human_players() == 1 && !application.is_player_human()) {
+            if (moves > 0) {
+                c = vstr[moves] - '0' - 1;
+                r = Board.instance.first_empty_row(c) + 1;
+                vstr[moves] = '0';
+                vstr[moves + 1] = '\0';
+                moves--;
+                swap_player();
+                move_cursor(c);
+                Board.instance.set(r, c, Tile.CLEAR);
+                GameBoardView.instance.draw_tile(r, c);
+            }
+        }
     }
-    return true;
-}
-
-void blink_winner(int n) {
-    /* blink the winner's line(s) n times */
 
-    if (winner == NOBODY)
-        return;
 
-    blink_t = winner;
-    if (is_hline_at(winner, row, column, &blink_r1, &blink_c1, &blink_r2, &blink_c2)) {
-        anim = AnimID.BLINK;
-        blink_on = false;
-        blink_n = n;
-        var temp = new Animate(0);
-        timeout = Timeout.add(SPEED_BLINK,  temp.exec);
-        while (timeout!=0)
-            Gtk.main_iteration();
+    void on_settings_preferences(SimpleAction action, Variant? parameter) {
+        prefsbox_open();
     }
 
-    if (is_vline_at(winner, row, column, &blink_r1, &blink_c1, &blink_r2, &blink_c2)) {
-        anim = AnimID.BLINK;
-        blink_on = false;
-        blink_n = n;
-        var temp = new Animate(0);
-        timeout = Timeout.add(SPEED_BLINK,  temp.exec);
-        while (timeout!=0)
-            Gtk.main_iteration();
-    }
+    void on_help_about(SimpleAction action, Variant? parameter) {
+        const string authors[] = {"Tim Musson <trmusson ihug co nz>",
+            "David Neary <bolsh gimp org>",
+            "Nikhar Agrawal <nikharagrawal2006 gmail com>",
+            "Jacob Humphrey <jacob ryan humphrey gmail com"
+        };
 
-    if (is_dline1_at(winner, row, column, &blink_r1, &blink_c1, &blink_r2, &blink_c2)) {
-        anim = AnimID.BLINK;
-        blink_on = false;
-        blink_n = n;
-        var temp = new Animate(0);
-        timeout = Timeout.add(SPEED_BLINK,  temp.exec);
-        while (timeout!=0)
-            Gtk.main_iteration();
-    }
+        const string artists[] = { "Alan Horkan",
+            "Anatol Drlicek",
+            "Based on the Faenza icon theme by Matthieu James"
+        };
 
-    if (is_dline2_at(winner, row, column, &blink_r1, &blink_c1, &blink_r2, &blink_c2)) {
-        anim = AnimID.BLINK;
-        blink_on = false;
-        blink_n = n;
-        var temp = new Animate(0);
-        timeout = Timeout.add(SPEED_BLINK,  temp.exec);
-        while (timeout!=0)
-            Gtk.main_iteration();
+        const string documenters[] = {"Timothy Musson"};
+
+        Gtk.show_about_dialog(window,
+            name: _(Config.APPNAME_LONG),
+            version: Config.VERSION,
+            copyright: "Copyright © 1999–2008 Tim Musson and David Neary" +
+                       "Copyright © 2014 Michael Catanzaro\nCopyright © 2018 Jacob Humphrey",
+            license_type: Gtk.License.GPL_2_0,
+            comments: _("Connect four in a row to win"),
+            authors: authors,
+            documenters: documenters,
+            artists: artists,
+            translator_credits: _("translator-credits"),
+            logo_icon_name: "four-in-a-row",
+            website: "https://wiki.gnome.org/Apps/Four-in-a-row";);
     }
-}
 
-bool on_button_press(Gtk.Widget w, Gdk.EventButton e) {
-    int x, y;
-    if (player_active) {
-        return false;
+    void on_help_contents(SimpleAction action, Variant? parameter) {
+        try {
+            Gtk.show_uri_on_window(window,
+                "help:four-in-a-row",
+                Gtk.get_current_event_time());
+        } catch(Error error) {
+            warning("Failed to show help: %s", error.message);
+        }
     }
 
-    if (gameover && timeout == 0) {
-        blink_winner(2);
-    } else if (is_player_human() && timeout == 0) {
-        w.get_window().get_device_position(e.device, out x, out y, null);
-        game_process_move(Gfx.get_column(x));
+    void check_game_state() {
+        if (Board.instance.is_line_at((Tile)player, row, column)) {
+            gameover = true;
+            winner = player;
+            switch (p.get_n_human_players()) {
+            case 1:
+                play_sound(is_player_human() ? SoundID.YOU_WIN : SoundID.I_WIN);
+                break;
+            case 0:
+            case 2:
+                play_sound(SoundID.PLAYER_WIN);
+                break;
+            }
+            blink_winner(6);
+        } else if (moves == 42) {
+            gameover = true;
+            winner = NOBODY;
+            play_sound(SoundID.DRAWN_GAME);
+        }
     }
 
-    return true;
-}
+    protected override void startup() {
+        base.startup();
+        Gtk.AspectFrame frame;
+        GLib.Menu app_menu, section;
 
-void scorebox_update() {
-    string s;
+        Gtk.Builder builder;
+        Gtk.CssProvider css_provider;
 
-    if (scorebox == null)
-        return;
+        Gtk.Window.set_default_icon_name("four-in-a-row");
 
-    if (get_n_human_players() == 1) {
-        if (p.level[PlayerID.PLAYER1] == Level.HUMAN) {
-            label_score[PlayerID.PLAYER1].set_text(_("You:"));
-            label_score[PlayerID.PLAYER2].set_text(_("Me:"));
-        } else {
-            label_score[PlayerID.PLAYER2].set_text(_("You:"));
-            label_score[PlayerID.PLAYER1].set_text(_("Me:"));
+        css_provider = new Gtk.CssProvider();
+        try {
+            css_provider.load_from_data("GtkButtonBox {-GtkButtonBox-child-internal-pad-x:0;}\0");
+        } catch (Error error) {
+            stderr.printf("Could not load UI: %s\n", error.message);
+            return;
         }
-    } else {
-        label_name[PlayerID.PLAYER1].set_text(theme_get_player(PlayerID.PLAYER1));
-        label_name[PlayerID.PLAYER2].set_text(theme_get_player(PlayerID.PLAYER2));
-    }
+        Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
+                                                 css_provider,
+                                                 Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
 
-    label_score[PlayerID.PLAYER1].set_text((string)score[PlayerID.PLAYER1]);
-    label_score[PlayerID.PLAYER2].set_text((string)score[PlayerID.PLAYER2]);
-    label_score[PlayerID.NOBODY].set_text((string)score[PlayerID.NOBODY]);
+        builder = new Gtk.Builder.from_file(Config.DATA_DIRECTORY + "/four-in-a-row.ui");
 
-}
+        window = builder.get_object("fiar-window") as Gtk.ApplicationWindow;
+        window.application = this;
+        window.set_default_size(DEFAULT_WIDTH, DEFAULT_HEIGHT); /* TODO save size & state */
 
-void on_settings_preferences(SimpleAction action, Variant? parameter) {
-    prefsbox_open();
-}
+        headerbar = builder.get_object("headerbar") as Gtk.HeaderBar;
 
-void on_game_hint(SimpleAction action, Variant? parameter) {
-    string s;
-    int c;
+        add_actions();
 
-    if (timeout != 0)
-        return;
-    if (gameover)
-        return;
+        app_menu = new GLib.Menu();
+        section = new GLib.Menu();
+        app_menu.append_section(null, section);
+        section.append(_("_Scores"), "app.scores");
+        section.append(_("_Preferences"), "app.preferences");
+        section = new GLib.Menu();
+        app_menu.append_section(null, section);
+        section.append(_("_Help"), "app.help");
+        section.append(_("_About"), "app.about");
+        section.append(_("_Quit"), "app.quit");
 
-    hint_action.set_enabled(false);
-    undo_action.set_enabled(false);
+        app_menu = app_menu;
 
-    set_status_message(_("I’m Thinking…"));
+        frame = builder.get_object("frame") as Gtk.AspectFrame;
 
-    vstr[0] = vlevel[Level.STRONG];
-    c = playgame((string)vstr) - 1;
+        frame.add(GameBoardView.instance);
 
-    column_moveto = c;
-    while (timeout != 0)
-        Gtk.main_iteration();
-    anim = AnimID.HINT;
-    var temp = new Animate(0);
-    timeout = Timeout.add(SPEED_MOVE, temp.exec);
+        hint_action.set_enabled(false);
+        undo_action.set_enabled(false);
+    }
 
-    blink_tile(0, c, gboard[0, c], 6);
+    Gtk.HeaderBar headerbar;
+}
 
-    s = _("Hint: Column ")+ (c + 1).to_string();
-    set_status_message(s);
-    g_free(s);
+SimpleAction hint_action;
+SimpleAction undo_action;
+SimpleAction new_game_action;
 
-    if (moves <= 0 || (moves == 1 && is_player_human()))
-        undo_action.set_enabled(false);
-    else
-        undo_action.set_enabled(true);
-}
+FourInARow? application;
+Gtk.ApplicationWindow window;
+Scorebox? scorebox = null;
 
-const GLib.ActionEntry app_entries[] = {
-    {"new-game", on_game_new, null, null, null},
-    {"undo-move", on_game_undo, null, null, null},
-    {"hint", on_game_hint, null, null, null},
-    {"scores", on_game_scores, null, null, null},
-    {"quit", on_game_exit, null, null, null},
-    {"preferences", on_settings_preferences, null, null, null},
-    {"help", on_help_contents, null, null, null},
-    {"about", on_help_about, null, null, null}
-};
+PlayerID player;
+PlayerID winner;
+PlayerID who_starts;
+int score[3];
+AnimID anim;
+char vstr[53];
+int moves;
+int column;
+int column_moveto;
+int row;
+int row_dropto;
+int blink_r1 = 0;
+int blink_c1 = 0;
+int blink_r2 = 0;
+int blink_c2 = 0;
+int blink_t = 0;
+int blink_n = 0;
+bool blink_on = false;
+uint timeout = 0;
 
-void create_app(GLib.Application app) {
-    Gtk.AspectFrame frame;
-    GLib.Menu app_menu, section;
+public int main(string[] argv) {
+    Intl.setlocale();
 
-    Gtk.Builder builder;
-    Gtk.CssProvider css_provider;
+    application = new FourInARow();
 
-    Gtk.Window.set_default_icon_name("four-in-a-row");
+    Intl.bindtextdomain(Config.GETTEXT_PACKAGE, Config.LOCALEDIR);
+    Intl.bind_textdomain_codeset(Config.GETTEXT_PACKAGE, "UTF-8");
+    Intl.textdomain(Config.GETTEXT_PACKAGE);
 
-    css_provider = new Gtk.CssProvider();
+    var context = new OptionContext();
+    context.add_group(Gtk.get_option_group(true));
     try {
-        css_provider.load_from_data("GtkButtonBox{-GtkButtonBox-child-internal-pad-x:0;}\0");
+        context.parse(ref argv);
     } catch (Error error) {
-        stderr.printf("Could not load UI: %s\n", error.message);
-        return;
+        print("%s", error.message);
+        return 1;
     }
-    Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
-                                             css_provider,
-                                             Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
-
-    builder = new Gtk.Builder.from_file(Config.DATA_DIRECTORY + "/four-in-a-row.ui");
-
-    window = (Gtk.Window) builder.get_object("fiar-window");
-    window.application = application;
-    window.set_default_size(DEFAULT_WIDTH, DEFAULT_HEIGHT); /* TODO save size & state */
-
-    headerbar = (Gtk.HeaderBar) builder.get_object("headerbar");
-
-    application.add_action_entries(app_entries, application);
-    application.add_accelerator("<Primary>n", "app.new-game", null);
-    application.add_accelerator("<Primary>h", "app.hint", null);
-    application.add_accelerator("<Primary>z", "app.undo-move", null);
-    application.add_accelerator("<Primary>q", "app.quit", null);
-    application.add_accelerator("F1", "app.contents", null);
-
-    app_menu = new GLib.Menu();
-    section = new GLib.Menu();
-    app_menu.append_section(null, section);
-    section.append(_("_Scores"), "app.scores");
-    section.append(_("_Preferences"), "app.preferences");
-    section = new GLib.Menu();
-    app_menu.append_section(null, section);
-    section.append(_("_Help"), "app.help");
-    section.append(_("_About"), "app.about");
-    section.append(_("_Quit"), "app.quit");
-
-    new_game_action = (GLib.SimpleAction) application.lookup_action("new-game");
-    undo_action = (GLib.SimpleAction) application.lookup_action("undo-move");
-    hint_action = (GLib.SimpleAction) application.lookup_action("hint");
-
-    application.app_menu = app_menu;
-
-    frame = (Gtk.AspectFrame) builder.get_object("frame");
-
-    drawarea = new Gtk.DrawingArea();
-    /* set a min size to avoid pathological behavior of gtk when scaling down */
-    drawarea.set_size_request(350, 350);
-    drawarea.halign = Gtk.Align.FILL;
-    drawarea.valign = Gtk.Align.FILL;
-    frame.add(drawarea);
-
-    drawarea.events = Gdk.EventMask.EXPOSURE_MASK |
-                      Gdk.EventMask.BUTTON_PRESS_MASK |
-                      Gdk.EventMask.BUTTON_RELEASE_MASK;
-    drawarea.configure_event.connect(on_drawarea_resize);
-    drawarea.draw.connect(on_drawarea_draw);
-    drawarea.button_press_event.connect(on_button_press);
-    drawarea.key_press_event.connect(on_key_press);
-
-    hint_action.set_enabled(false);
-    undo_action.set_enabled(false);
-}
 
-void on_dialog_close(Gtk.Widget w, int response_id) {
-    w.hide();
-}
+    settings = new GLib.Settings("org.gnome.four-in-a-row");
 
-void prompt_player() {
-    int players = get_n_human_players();
-    bool human = is_player_human();
-    string who;
-    string str;
+    Environment.set_application_name(_(Config.APPNAME_LONG));
 
-    hint_action.set_enabled(human && !gameover);
+    p = new Prefs();
+    application.game_init();
 
-    switch (players) {
-    case 0:
-        undo_action.set_enabled(false);
-        break;
-    case 1:
-        undo_action.set_enabled((human && moves >1) || (!human && gameover));
-        break;
-    case 2:
-        undo_action.set_enabled(moves > 0);
-        break;
-    }
+    if (!GameBoardView.instance.load_pixmaps())
+        return 1;
 
-    if (gameover && winner == PlayerID.NOBODY) {
-        if (score[PlayerID.NOBODY] == 0)
-            set_status_message(null);
-        else
-            set_status_message(_("It’s a draw!"));
-        return;
-    }
+    var app_retval = application.run(argv);
 
-    switch (players) {
-    case 1:
-        if (human) {
-            if (gameover)
-                set_status_message(_("You win!"));
-            else
-                set_status_message(_("Your Turn"));
-        } else {
-            if (gameover)
-                set_status_message(_("I win!"));
-            else
-                set_status_message(_("I’m Thinking…"));
-        }
-        break;
-    case 2:
-    case 0:
+    return app_retval;
+}
 
-        if (gameover) {
-            who = player == PLAYER1 ? theme_get_player_win(PlayerID.PLAYER1)
-                : theme_get_player_win(PlayerID.PLAYER2);
-            str =  _(who);
-        } else if (player_active) {
-            set_status_message(_("Your Turn"));
-            return;
-        } else {
-            who = player == PLAYER1 ? theme_get_player_turn(PlayerID.PLAYER1)
-                : theme_get_player_turn(PlayerID.PLAYER2);
-            str =  _(who);
-        }
 
-        set_status_message(str);
-        break;
-    }
-}
diff --git a/src/prefs.vala b/src/prefs.vala
index 567d99b..37df47d 100644
--- a/src/prefs.vala
+++ b/src/prefs.vala
@@ -19,11 +19,65 @@
  * along with GNOME Four-in-a-row. If not, see <http://www.gnu.org/licenses/>.
  */
 
-struct Prefs {
-    bool do_sound;
-    int theme_id;
-    Level level[2];
-    int keypress[3];
+class Prefs {
+    public bool do_sound;
+    public int theme_id;
+    public Level level[2];
+    public int keypress[3];
+
+    public Prefs() {
+        do_sound = settings.get_boolean("sound");
+        level[PlayerID.PLAYER1] = Level.HUMAN; /* Human. Always human. */
+        level[PlayerID.PLAYER2] = (Level) settings.get_int("opponent");
+        keypress[Move.LEFT] = settings.get_int("key-left");
+        keypress[Move.RIGHT] = settings.get_int("key-right");
+        keypress[Move.DROP] = settings.get_int("key-drop");
+        theme_id = settings.get_int("theme-id");
+
+        settings.changed.connect(settings_changed_cb);
+
+        level[PlayerID.PLAYER1] = sane_player_level(level[PlayerID.PLAYER1]);
+        level[PlayerID.PLAYER2] = sane_player_level(level[PlayerID.PLAYER2]);
+        theme_id = sane_theme_id(theme_id);
+    }
+
+    int sane_theme_id(int val) {
+        if (val < 0 || val >= theme.length)
+            return DEFAULT_THEME_ID;
+            return val;
+    }
+
+    public void settings_changed_cb(string key) {
+        if (key == "sound") {
+            p.do_sound = settings.get_boolean("sound");
+            ((Gtk.ToggleButton)checkbutton_sound).set_active(p.do_sound);
+        } else if (key == "key-left") {
+            p.keypress[Move.LEFT] = settings.get_int("key-left");
+        } else if (key == "key-right") {
+            p.keypress[Move.RIGHT] = settings.get_int("key-right");
+        } else if (key == "key-drop") {
+            p.keypress[Move.DROP] = settings.get_int("key-drop");
+        } else if (key == "theme-id") {
+            int val = sane_theme_id(settings.get_int("theme-id"));
+            if (val != p.theme_id) {
+                p.theme_id = val;
+                if (!GameBoardView.instance.change_theme())
+                    return;
+                if (prefsbox == null)
+                    return;
+                combobox_theme.set_active(p.theme_id);
+            }
+        }
+    }
+
+    public int get_n_human_players() {
+        if (level[PlayerID.PLAYER1] != Level.HUMAN && level[PlayerID.PLAYER2] != Level.HUMAN)
+            return 0;
+        if (level[PlayerID.PLAYER1] != Level.HUMAN || level[PlayerID.PLAYER2] != Level.HUMAN)
+            return 1;
+        return 2;
+    }
+
 }
 
 Settings settings;
@@ -43,13 +97,8 @@ const uint DEFAULT_KEY_RIGHT = Gdk.Key.Right;
 const uint DEFAULT_KEY_DROP = Gdk.Key.Down;
 const int DEFAULT_THEME_ID = 0;
 
-static int sane_theme_id(int val) {
-    if (val < 0 || val >= theme.length)
-    return DEFAULT_THEME_ID;
-    return val;
-}
 
-public int sane_player_level(int val) {
+public Level sane_player_level(Level val) {
     if (val < Level.HUMAN)
         return Level.HUMAN;
     if (val > Level.STRONG)
@@ -67,45 +116,6 @@ public void on_toggle_sound(Gtk.ToggleButton t) {
     settings.set_boolean("sound", t.get_active());
 }
 
-void prefs_init() {
-    p.do_sound = settings.get_boolean("sound");
-    p.level[PlayerID.PLAYER1] = Level.HUMAN; /* Human. Always human. */
-    p.level[PlayerID.PLAYER2] = (Level) settings.get_int("opponent");
-    p.keypress[Move.LEFT] = settings.get_int("key-left");
-    p.keypress[Move.RIGHT] = settings.get_int("key-right");
-    p.keypress[Move.DROP] = settings.get_int("key-drop");
-    p.theme_id = settings.get_int("theme-id");
-
-    settings.changed.connect(settings_changed_cb);
-
-    p.level[PlayerID.PLAYER1] = (Level) sane_player_level(p.level[PlayerID.PLAYER1]);
-    p.level[PlayerID.PLAYER2] = (Level) sane_player_level(p.level[PlayerID.PLAYER2]);
-    p.theme_id = sane_theme_id(p.theme_id);
-}
-
-public void settings_changed_cb(string key) {
-    if (key == "sound") {
-        p.do_sound = settings.get_boolean("sound");
-        ((Gtk.ToggleButton)checkbutton_sound).set_active(p.do_sound);
-    } else if (key == "key-left") {
-        p.keypress[Move.LEFT] = settings.get_int("key-left");
-    } else if (key == "key-right") {
-        p.keypress[Move.RIGHT] = settings.get_int("key-right");
-    } else if (key == "key-drop") {
-        p.keypress[Move.DROP] = settings.get_int("key-drop");
-    } else if (key == "theme-id") {
-        int val = sane_theme_id(settings.get_int("theme-id"));
-        if (val != p.theme_id) {
-            p.theme_id = val;
-            if (!Gfx.change_theme())
-                return;
-        if (prefsbox == null)
-            return;
-        combobox_theme.set_active(p.theme_id);
-        }
-    }
-}
-
 public void on_select_opponent(Gtk.ComboBox w) {
     Gtk.TreeIter iter;
     int value;
@@ -115,9 +125,9 @@ public void on_select_opponent(Gtk.ComboBox w) {
 
     p.level[PlayerID.PLAYER2] = (Level)value;
     settings.set_int("opponent", value);
-    scorebox_reset();
+    scorebox.reset();
     who_starts = PlayerID.PLAYER2; /* This gets reversed in game_reset. */
-    game_reset();
+    application.game_reset();
 }
 
 public void prefsbox_open() {
diff --git a/src/scorebox.vala b/src/scorebox.vala
new file mode 100644
index 0000000..9ac7b8d
--- /dev/null
+++ b/src/scorebox.vala
@@ -0,0 +1,92 @@
+
+/*
+ * Needed to force vala to include headers in the correct order.
+ * See https://gitlab.gnome.org/GNOME/vala/issues/98
+ */
+const string scorebox_gettext_package = Config.GETTEXT_PACKAGE;
+
+class Scorebox : Gtk.Dialog {
+    Gtk.Label label_name[3];
+    Gtk.Label label_score[3];
+
+    public Scorebox() {
+        Object(title: _("Scores"),
+               parent: window,
+               use_header_bar: 1,
+               destroy_with_parent: true,
+               resizable: false,
+               border_width: 5);
+        get_content_area().spacing = 2;
+
+        Gtk.Grid grid, grid2;
+
+        grid = new Gtk.Grid();
+        grid.halign = Gtk.Align.CENTER;
+        grid.row_spacing = 6;
+        grid.orientation = Gtk.Orientation.VERTICAL;
+        grid.border_width = 5;
+
+        get_content_area().pack_start(grid);
+
+        grid2 = new Gtk.Grid();
+        grid.add(grid2);
+        grid2.column_spacing = 6;
+
+        label_name[PlayerID.PLAYER1] = new Gtk.Label(null);
+        grid2.attach(label_name[PlayerID.PLAYER1], 0, 0, 1, 1);
+        label_name[PlayerID.PLAYER1].xalign = 0;
+        label_name[PlayerID.PLAYER1].yalign = 0.5f;
+
+        label_score[PlayerID.PLAYER1] = new Gtk.Label(null);
+        grid2.attach(label_score[PlayerID.PLAYER1], 1, 0, 1, 1);
+        label_score[PlayerID.PLAYER1].xalign = 0;
+        label_score[PlayerID.PLAYER1].yalign = 0.5f;
+
+        label_name[PlayerID.PLAYER2] = new Gtk.Label(null);
+        grid2.attach(label_name[PlayerID.PLAYER2], 0, 1, 1, 1);
+        label_name[PlayerID.PLAYER2].xalign = 0;
+        label_name[PlayerID.PLAYER2].yalign = 0.5f;
+
+        label_score[PlayerID.PLAYER2] = new Gtk.Label(null);
+        grid2.attach(label_score[PlayerID.PLAYER2], 1, 0, 1, 1);
+        label_score[PlayerID.PLAYER2].set_xalign(0);
+        label_score[PlayerID.PLAYER2].set_yalign(0.5f);
+
+        label_name[PlayerID.NOBODY] = new Gtk.Label(_("Drawn:"));
+        grid2.attach(label_name[PlayerID.NOBODY], 0, 2, 1, 1);
+        label_name[PlayerID.NOBODY].set_xalign(0);
+        label_name[PlayerID.NOBODY].set_yalign(0.5f);
+
+        label_score[PlayerID.NOBODY] = new Gtk.Label(null);
+        grid2.attach(label_score[PlayerID.NOBODY], 1, 0, 1, 1);
+        label_score[PlayerID.NOBODY].set_xalign(0);
+        label_score[PlayerID.NOBODY].set_yalign(0.5f);
+    }
+
+    public void update() {
+        if (p.get_n_human_players() == 1) {
+            if (p.level[PlayerID.PLAYER1] == Level.HUMAN) {
+                label_score[PlayerID.PLAYER1].label = _("You:");
+                label_score[PlayerID.PLAYER2].label = _("Me:");
+            } else {
+                label_score[PlayerID.PLAYER2].label = _("You:");
+                label_score[PlayerID.PLAYER1].label = _("Me:");
+            }
+        } else {
+            label_name[PlayerID.PLAYER1].label = theme_get_player(PlayerID.PLAYER1);
+            label_name[PlayerID.PLAYER2].label = theme_get_player(PlayerID.PLAYER2);
+        }
+
+        label_score[PlayerID.PLAYER1].label = (string)score[PlayerID.PLAYER1];
+        label_score[PlayerID.PLAYER2].label = (string)score[PlayerID.PLAYER2];
+        label_score[PlayerID.NOBODY].label = (string)score[PlayerID.NOBODY];
+
+    }
+
+    public void reset() {
+        score[PlayerID.PLAYER1] = 0;
+        score[PlayerID.PLAYER2] = 0;
+        score[PlayerID.NOBODY] = 0;
+        update();
+    }
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]