[gnome-games] quadrapassel: Port from C++ to Vala



commit 44c53b0b3841cee1b6d06ebca88026ef51d29af7
Author: Robert Ancell <robert ancell canonical com>
Date:   Wed Jan 4 16:03:56 2012 +1100

    quadrapassel: Port from C++ to Vala

 configure.ac                                       |   20 +-
 libgames-support/GnomeGamesSupport-1.0.vapi        |    8 +
 quadrapassel/data/7blocks-gw.png                   |  Bin 2157 -> 0 bytes
 quadrapassel/data/7blocks-tig.png                  |  Bin 9079 -> 0 bytes
 quadrapassel/data/Makefile.am                      |   17 +-
 quadrapassel/data/gameover.ogg                     |  Bin 0 -> 8175 bytes
 {sounds => quadrapassel/data}/land.ogg             |  Bin 5520 -> 5520 bytes
 {sounds => quadrapassel/data}/lines1.ogg           |  Bin 7916 -> 7916 bytes
 {sounds => quadrapassel/data}/lines2.ogg           |  Bin 8315 -> 8315 bytes
 {sounds => quadrapassel/data}/lines3.ogg           |  Bin 9457 -> 9457 bytes
 .../data/org.gnome.quadrapassel.gschema.xml.in     |   15 +-
 .../data/quadrapassel.ogg                          |  Bin 13542 -> 13542 bytes
 quadrapassel/data/quadrapassel.svg                 |   99 --
 sounds/land.ogg => quadrapassel/data/slide.ogg     |  Bin 5520 -> 4036 bytes
 {sounds => quadrapassel/data}/turn.ogg             |  Bin 6091 -> 6091 bytes
 quadrapassel/src/Makefile.am                       |   54 +-
 quadrapassel/src/blockops.cpp                      |  894 --------------
 quadrapassel/src/blockops.h                        |  145 ---
 quadrapassel/src/blocks-cache.cpp                  |  379 ------
 quadrapassel/src/blocks-cache.h                    |   70 --
 quadrapassel/src/blocks.cpp                        |  398 ------
 quadrapassel/src/blocks.h                          |   73 --
 quadrapassel/src/config.vapi                       |    5 +
 quadrapassel/src/game-view.vala                    |  592 +++++++++
 quadrapassel/src/game.vala                         |  745 +++++++++++
 quadrapassel/src/highscores.cpp                    |   53 -
 quadrapassel/src/highscores.h                      |   41 -
 quadrapassel/src/main.cpp                          |   89 --
 quadrapassel/src/preview.cpp                       |  154 ---
 quadrapassel/src/preview.h                         |   66 -
 quadrapassel/src/preview.vala                      |  111 ++
 quadrapassel/src/quadrapassel.vala                 |  696 +++++++++++
 quadrapassel/src/renderer.cpp                      |  296 -----
 quadrapassel/src/renderer.h                        |   64 -
 quadrapassel/src/scoreframe.cpp                    |  163 ---
 quadrapassel/src/scoreframe.h                      |   75 --
 quadrapassel/src/sound.cpp                         |   61 -
 quadrapassel/src/sound.h                           |   32 -
 quadrapassel/src/tetris.cpp                        | 1304 --------------------
 quadrapassel/src/tetris.h                          |  175 ---
 sounds/Makefile.am                                 |    5 -
 41 files changed, 2199 insertions(+), 4700 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 757b5b1..040ffb8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -107,7 +107,6 @@ AC_SUBST([gamelist])
 
 # Feature matrix
 
-need_cxx=no
 need_vala=no
 need_rsvg=no
 need_sqlite=no
@@ -118,11 +117,7 @@ need_clutter=no
 
 for game in $gamelist; do
   case $game in
-    quadrapassel) need_cxx=yes ;;
-    *) ;;
-  esac
-  case $game in
-    glchess|gnomine|gnotravex|iagno|lightsoff|mahjongg) need_vala=yes ;;
+    glchess|gnomine|gnotravex|iagno|lightsoff|mahjongg|quadrapassel) need_vala=yes ;;
     *) ;;
   esac
   case $game in
@@ -189,16 +184,6 @@ if test "$need_vala" = "yes"; then
   AM_PROG_VALAC([0.13.0])
 fi
 
-if test "$need_cxx" = "yes"; then
-  AC_PROG_CXX
-
-  # Check whether a C++ was found (AC_PROG_CXX sets $CXX to "g++" even when it
-  # doesn't exist)
-  AC_LANG_PUSH([C++])
-  AC_COMPILE_IFELSE([AC_LANG_PROGRAM([],[])],[],[AC_MSG_ERROR([No C++ compiler found])])
-  AC_LANG_POP([C++])
-fi
-
 AM_PROG_CC_C_O
 
 LT_INIT
@@ -206,7 +191,6 @@ LT_INIT
 GNOME_COMMON_INIT
 GNOME_DEBUG_CHECK
 GNOME_COMPILE_WARNINGS([maximum])
-GNOME_CXX_WARNINGS([yes])
 GNOME_MAINTAINER_MODE_DEFINES
 
 dnl ****************************************************************************
@@ -412,7 +396,6 @@ PKG_CHECK_MODULES([CANBERRA_GTK],[libcanberra-gtk3 >= $LIBCANBERRA_GTK_REQUIRED]
 # ********
 
 AM_CFLAGS="$AM_CFLAGS $WARN_CFLAGS"
-AM_CXXFLAGS="$AM_CXXFLAGS $WARN_CXXFLAGS"
 
 # ****
 # i18n
@@ -485,7 +468,6 @@ GOBJECT_INTROSPECTION_CHECK([0.6.3])
 
 AC_SUBST([AM_CPPFLAGS])
 AC_SUBST([AM_CFLAGS])
-AC_SUBST([AM_CXXFLAGS])
 AC_SUBST([AM_LDFLAGS])
 
 ##############################################
diff --git a/libgames-support/GnomeGamesSupport-1.0.vapi b/libgames-support/GnomeGamesSupport-1.0.vapi
index 4a9b154..3eda331 100644
--- a/libgames-support/GnomeGamesSupport-1.0.vapi
+++ b/libgames-support/GnomeGamesSupport-1.0.vapi
@@ -195,5 +195,13 @@ namespace GnomeGamesSupport
         public unowned string? get_nth (int n);
         public Gtk.Widget create_widget (string selection, uint flags);
     }
+
+    [CCode (cheader_filename = "games-controls.h")]
+    public class ControlsList : Gtk.ScrolledWindow
+    {
+        public ControlsList (GLib.Settings settings);
+        public void add_control (string conf_key, string label, uint default_keyval);
+        public void add_controls (string first_conf_key, ...);
+    }
 }
 
diff --git a/quadrapassel/data/Makefile.am b/quadrapassel/data/Makefile.am
index aabbc4b..ddd1670 100644
--- a/quadrapassel/data/Makefile.am
+++ b/quadrapassel/data/Makefile.am
@@ -5,11 +5,16 @@ gsettings_SCHEMAS = $(gsettings_in_file:.xml.in=.xml)
 @INTLTOOL_XML_NOMERGE_RULE@
 @GSETTINGS_RULES@
 
-pixmapdir = $(datadir)/quadrapassel/pixmaps
-pixmap_DATA = \
-	quadrapassel.svg \
-	7blocks-tig.png \
-	7blocks-gw.png
+soundsdir = $(datadir)/quadrapassel/sounds
+sounds_DATA = \
+	gameover.ogg \
+	land.ogg \
+	lines1.ogg \
+	lines2.ogg \
+	lines3.ogg \
+	slide.ogg \
+	turn.ogg \
+	quadrapassel.ogg
 
 desktopdir = $(datadir)/applications
 desktop_in_files = quadrapassel.desktop.in.in
@@ -20,7 +25,7 @@ man_MANS = quadrapassel.6
 
 EXTRA_DIST = \
 	$(gsettings_in_file) \
-	$(pixmap_DATA) \
+	$(sounds_DATA) \
 	$(desktop_in_files) \
 	$(man_MANS)
 
diff --git a/quadrapassel/data/gameover.ogg b/quadrapassel/data/gameover.ogg
new file mode 100644
index 0000000..4858088
Binary files /dev/null and b/quadrapassel/data/gameover.ogg differ
diff --git a/sounds/land.ogg b/quadrapassel/data/land.ogg
similarity index 100%
copy from sounds/land.ogg
copy to quadrapassel/data/land.ogg
diff --git a/sounds/lines1.ogg b/quadrapassel/data/lines1.ogg
similarity index 100%
rename from sounds/lines1.ogg
rename to quadrapassel/data/lines1.ogg
diff --git a/sounds/lines2.ogg b/quadrapassel/data/lines2.ogg
similarity index 100%
rename from sounds/lines2.ogg
rename to quadrapassel/data/lines2.ogg
diff --git a/sounds/lines3.ogg b/quadrapassel/data/lines3.ogg
similarity index 100%
rename from sounds/lines3.ogg
rename to quadrapassel/data/lines3.ogg
diff --git a/quadrapassel/data/org.gnome.quadrapassel.gschema.xml.in b/quadrapassel/data/org.gnome.quadrapassel.gschema.xml.in
index 80adf4d..a318800 100644
--- a/quadrapassel/data/org.gnome.quadrapassel.gschema.xml.in
+++ b/quadrapassel/data/org.gnome.quadrapassel.gschema.xml.in
@@ -6,25 +6,16 @@
       <_description>Image to use for drawing blocks.</_description>
     </key>
     <key name="theme" type="s">
-      <default>'tangoshaded'</default>
+      <default>'clean'</default>
       <_summary>The theme used for rendering the blocks</_summary>
       <_description>The name of the theme used for rendering the blocks and the background.</_description>
     </key>
     <key name="starting-level" type="i">
       <default>1</default>
+      <range min="1" max="20"/>
       <_summary>Level to start with</_summary>
       <_description>Level to start with.</_description>
     </key>
-    <key name="use-bg-image" type="b">
-      <default>true</default>
-      <_summary>Whether to use the background image</_summary>
-      <_description>This selects whether or not to draw the background image over the background color.</_description>
-    </key>
-    <key name="bg-color" type="s">
-      <default>'Black'</default>
-      <_summary>The background color</_summary>
-      <_description>The background color, in a format gdk_color_parse understands.</_description>
-    </key>
     <key name="do-preview" type="b">
       <default>true</default>
       <_summary>Whether to preview the next block</_summary>
@@ -47,11 +38,13 @@
     </key>
     <key name="line-fill-height" type="i">
       <default>0</default>
+      <range min="0" max="19"/>
       <_summary>The number of rows to fill</_summary>
       <_description>The number of rows that are filled with random blocks at the start of the game.</_description>
     </key>
     <key name="line-fill-probability" type="i">
       <default>5</default>
+      <range min="0" max="10"/>
       <_summary>The density of filled rows</_summary>
       <_description>The density of blocks in rows filled at the start of the game. The value is between 0 (for no blocks) and 10 (for a completely filled row).</_description>
     </key>
diff --git a/sounds/gnometris.ogg b/quadrapassel/data/quadrapassel.ogg
similarity index 100%
rename from sounds/gnometris.ogg
rename to quadrapassel/data/quadrapassel.ogg
diff --git a/sounds/land.ogg b/quadrapassel/data/slide.ogg
similarity index 57%
rename from sounds/land.ogg
rename to quadrapassel/data/slide.ogg
index e322fb6..029d6db 100644
Binary files a/sounds/land.ogg and b/quadrapassel/data/slide.ogg differ
diff --git a/sounds/turn.ogg b/quadrapassel/data/turn.ogg
similarity index 100%
rename from sounds/turn.ogg
rename to quadrapassel/data/turn.ogg
diff --git a/quadrapassel/src/Makefile.am b/quadrapassel/src/Makefile.am
index 32fc56d..5e31bfa 100644
--- a/quadrapassel/src/Makefile.am
+++ b/quadrapassel/src/Makefile.am
@@ -1,39 +1,37 @@
 bin_PROGRAMS = quadrapassel
 
 quadrapassel_SOURCES = \
-	main.cpp \
-	blocks.cpp \
-	blocks.h \
-	highscores.cpp \
-	highscores.h \
-	scoreframe.cpp \
-	scoreframe.h \
-	tetris.cpp \
-	tetris.h \
-	preview.cpp \
-	preview.h \
-	blockops.cpp \
-	blockops.h \
-	renderer.cpp \
-	renderer.h \
-	blocks-cache.cpp \
-	blocks-cache.h \
-	sound.cpp \
-	sound.h
+	config.vapi \
+	fixes.vapi \
+	quadrapassel.vala \
+	preview.vala \
+	game.vala \
+	game-view.vala
 
-quadrapassel_CPPFLAGS = \
-	-I$(top_srcdir) \
-	$(AM_CPPFLAGS)
-
-quadrapassel_CXXFLAGS = \
+quadrapassel_CFLAGS = \
+	-I$(top_srcdir)/libgames-support \
+	-DVERSION=\"$(VERSION)\" \
+	-DGETTEXT_PACKAGE=\"$(GETTEXT_PACKAGE)\" \
 	-DDATA_DIRECTORY=\"$(datadir)/quadrapassel\" \
-	-DSOUND_DIRECTORY=\"$(pkgdatadir)/sounds\" \
+	-DSOUND_DIRECTORY=\"$(datadir)/quadrapassel/sounds\" \
 	-DLOCALEDIR=\"$(datadir)/locale\" \
 	$(GTK_CFLAGS) \
 	$(CANBERRA_GTK_CFLAGS) \
 	$(CLUTTER_GTK_CFLAGS) \
-	$(CLUTTER_CFLAGS) \
-	$(AM_CXXFLAGS)
+	$(CLUTTER_CFLAGS)
+
+quadrapassel_VALAFLAGS = \
+	--pkg posix \
+	--pkg gtk+-3.0 \
+	--pkg pango \
+	--pkg pangocairo \
+	--pkg clutter-1.0 \
+	--pkg clutter-gtk-1.0 \
+	--pkg cogl-1.0 \
+	--pkg libcanberra \
+	--pkg libcanberra-gtk \
+	--vapidir $(top_srcdir)/libgames-support \
+	--pkg GnomeGamesSupport-1.0
 
 quadrapassel_LDADD = \
 	$(top_builddir)/libgames-support/libgames-support.la \
@@ -44,7 +42,7 @@ quadrapassel_LDADD = \
 	$(INTLLIBS)
 
 if HAVE_RSVG
-quadrapassel_CXXFLAGS += $(RSVG_CFLAGS) 
+quadrapassel_CFLAGS += $(RSVG_CFLAGS) 
 quadrapassel_LDADD += $(RSVG_LIBS)
 endif
 
diff --git a/quadrapassel/src/config.vapi b/quadrapassel/src/config.vapi
new file mode 100644
index 0000000..3e558f2
--- /dev/null
+++ b/quadrapassel/src/config.vapi
@@ -0,0 +1,5 @@
+public const string VERSION;
+public const string GETTEXT_PACKAGE;
+public const string DATA_DIRECTORY;
+public const string SOUND_DIRECTORY;
+public const string LOCALEDIR;
diff --git a/quadrapassel/src/game-view.vala b/quadrapassel/src/game-view.vala
new file mode 100644
index 0000000..ef42a06
--- /dev/null
+++ b/quadrapassel/src/game-view.vala
@@ -0,0 +1,592 @@
+public class GameView : GtkClutter.Embed
+{
+    /* Game being played */
+    private Game? _game = null;
+    public Game? game
+    {
+        get { return _game; }
+        set
+        {
+            if (_game != null)
+                SignalHandler.disconnect_matched (_game, SignalMatchType.DATA, 0, 0, null, null, this);
+            _game = value;
+            _game.shape_added.connect (shape_added_cb);
+            _game.shape_moved.connect (shape_moved_cb);
+            _game.shape_dropped.connect (shape_dropped_cb);
+            _game.shape_rotated.connect (shape_rotated_cb);
+            _game.shape_landed.connect (shape_landed_cb);
+            _game.pause_changed.connect (pause_changed_cb);
+            _game.complete.connect (game_complete_cb);
+
+            /* Remove any existing block */
+            blocks.remove_all ();
+            playing_field.remove_all ();
+
+            /* Add in the current blocks */
+            if (game.shape != null)
+                shape_added_cb ();
+            for (var x = 0; x < _game.width; x++)
+            {
+                for (var y = 0; y < _game.height; y++)
+                {
+                    var block = _game.blocks[x, y];
+                    if (block != null)
+                    {
+                        var actor = new BlockActor (block, block_textures[block.color]);
+                        blocks.insert (block, actor);
+                        actor.set_size (cell_size, cell_size);
+                        actor.set_position (block.x * cell_size, block.y * cell_size);
+                        playing_field.add (actor);
+                    }
+                }
+            }
+
+            set_size_request (_game.width * 190 / _game.height, 190);
+            update_message ();
+        }
+    }
+
+    /* false to play sound effects */
+    public bool mute;
+
+    /* Theme to use */
+    public string theme
+    {
+        set
+        {
+            foreach (var texture in block_textures)
+                texture.theme = value;
+        }
+    }
+
+    private Clutter.Group playing_field;
+
+    /* The shape currently falling */
+    private Clutter.Group? shape = null;
+
+    /* Overlay to draw messages on */
+    private TextOverlay text_overlay;
+
+    /* Textures used to draw blocks */
+    private BlockTexture[] block_textures;
+
+    /* Blocks */
+    private HashTable<Block, BlockActor> blocks;
+    private HashTable<Block, BlockActor> shape_blocks;
+
+    /* Number of lines destroyed (required for earthquake effect) */
+    private int n_lines_destroyed;
+
+    private int cell_size
+    {
+        get
+        {
+            if (game != null)
+                return int.min (get_allocated_width () / game.width, get_allocated_height () / game.height);
+            else
+                return 0;
+        }
+    }
+
+    public GameView ()
+    {
+        blocks = new HashTable<Block, BlockActor> (direct_hash, direct_equal);
+        shape_blocks = new HashTable<Block, BlockActor> (direct_hash, direct_equal);
+
+        size_allocate.connect (size_allocate_cb);
+
+        var stage = (Clutter.Stage) get_stage ();
+        Clutter.Color stage_color = { 0x0, 0x0, 0x0, 0xff };
+        stage.set_color (stage_color);
+
+        playing_field = new Clutter.Group ();
+        stage.add_actor (playing_field);
+
+        text_overlay = new TextOverlay ();
+        stage.add (text_overlay);
+
+        block_textures = new BlockTexture[NCOLORS];
+        for (var i = 0; i < block_textures.length; i++)
+        {
+            // FIXME: Have to set a size to avoid an assertion in Clutter
+            block_textures[i] = new BlockTexture (i, 1);
+            block_textures[i].hide ();
+            stage.add_actor (block_textures[i]);
+        }
+    }
+
+    private void play_sound (string name)
+    {
+        if (!mute)
+            CanberraGtk.play_for_widget (this, 0,
+                                         Canberra.PROP_MEDIA_NAME, name,
+                                         Canberra.PROP_MEDIA_FILENAME, Path.build_filename (SOUND_DIRECTORY, "%s.ogg".printf (name)));
+    }
+
+    private void shape_added_cb ()
+    {
+        shape = new Clutter.Group ();
+        playing_field.add (shape);
+        shape.set_position (game.shape.x * cell_size, game.shape.y * cell_size);
+        foreach (var block in game.shape.blocks)
+        {
+            var actor = new BlockActor (block, block_textures[block.color]);
+            shape_blocks.insert (block, actor);
+            shape.add (actor);
+            actor.set_size (cell_size, cell_size);
+            actor.set_position (block.x * cell_size, block.y * cell_size);
+        }
+    }
+
+    private void shape_moved_cb ()
+    {
+        play_sound ("slide");
+        shape.animate (Clutter.AnimationMode.EASE_IN_QUAD, 30, "x", (float) game.shape.x * cell_size);
+    }
+
+    private void shape_dropped_cb ()
+    {
+        shape.animate (Clutter.AnimationMode.EASE_IN_QUAD, 60, "y", (float) game.shape.y * cell_size);
+    }
+
+    private void shape_rotated_cb ()
+    {
+        play_sound ("turn");
+        foreach (var block in game.shape.blocks)
+        {
+            var actor = shape_blocks.lookup (block);
+            actor.set_position (block.x * cell_size, block.y * cell_size);
+        }
+    }
+
+    private void shape_landed_cb (int[] lines, List<Block> line_blocks)
+    {
+        switch (lines.length)
+        {
+        default:
+            play_sound ("land");
+            break;
+        case 1:
+            play_sound ("lines1");
+            break;
+        case 2:
+            play_sound ("lines2");
+            break;
+        case 3:
+        case 4:
+            play_sound ("lines3");
+            break;
+        }
+
+        /* Remove the moving shape */
+        shape.destroy ();
+        shape = null;
+        shape_blocks.remove_all ();
+
+        /* Land the shape blocks */
+        foreach (var block in game.shape.blocks)
+        {
+            var actor = new BlockActor (block, block_textures[block.color]);
+            playing_field.add (actor);
+            blocks.insert (block, actor);
+            actor.set_size (cell_size, cell_size);
+            actor.set_position (block.x * cell_size, block.y * cell_size);
+        }
+
+        /* Explode blocks */
+        foreach (var block in line_blocks)
+        {
+            var actor = blocks.lookup (block);
+            actor.explode ();
+            blocks.remove (block);
+        }
+
+        /* Drop blocks that have moved */
+        if (lines.length > 0)
+        {
+            var timeline = new Clutter.Timeline (60);
+            n_lines_destroyed = lines.length;
+            timeline.completed.connect (fall_completed_cb);
+            for (var x = 0; x < game.width; x++)
+            {
+                for (var y = 0; y < game.height; y++)
+                {
+                    var block = game.blocks[x, y];
+                    if (block == null)
+                        continue;
+
+                    var actor = blocks.lookup (block);
+                    actor.animate_with_timeline (Clutter.AnimationMode.EASE_IN_QUAD, timeline, "x", (float) block.x * cell_size, "y", (float) block.y * cell_size);
+                }
+            }
+        }
+    }
+
+    private void fall_completed_cb (Clutter.Timeline timeline)
+    {
+        /* Do an earthquake effect */
+        float x, y;
+        playing_field.get_position (out x, out y);
+        playing_field.set_position (x, y + cell_size * n_lines_destroyed * 0.25f);
+        playing_field.animate (Clutter.AnimationMode.EASE_OUT_BOUNCE, 720 / (5 - n_lines_destroyed), "x", x, "y", y);
+    }
+
+    private void size_allocate_cb (Gtk.Widget widget, Gtk.Allocation allocation)
+    {
+        if (game == null)
+            return;
+
+        foreach (var texture in block_textures)
+            texture.set_size (cell_size, cell_size);
+
+        var iter = HashTableIter<Block, BlockActor> (blocks);
+        while (true)
+        {
+            Block block;
+            BlockActor actor;
+            if (!iter.next (out block, out actor))
+                break;
+            actor.set_size (cell_size, cell_size);
+            actor.set_position (block.x * cell_size, block.y * cell_size);
+        }
+        var shape_iter = HashTableIter<Block, BlockActor> (shape_blocks);
+        while (true)
+        {
+            Block block;
+            BlockActor actor;
+            if (!shape_iter.next (out block, out actor))
+                break;
+            actor.set_size (cell_size, cell_size);
+            actor.set_position (block.x * cell_size, block.y * cell_size);
+        }
+        if (shape != null)
+            shape.set_position (game.shape.x * cell_size, game.shape.y * cell_size);
+
+        text_overlay.set_size (get_allocated_width (), get_allocated_height ());
+        text_overlay.raise_top ();
+
+        playing_field.set_size (game.width * cell_size, game.height * cell_size);
+        playing_field.set_position ((get_allocated_width () - playing_field.get_width ()) * 0.5f,
+                                    get_allocated_height () - playing_field.get_height ());
+    }
+
+    private void pause_changed_cb ()
+    {
+        update_message ();
+    }
+
+    private void game_complete_cb ()
+    {
+        play_sound ("gameover");
+        update_message ();
+    }
+
+    private void update_message ()
+    {
+        if (game.paused)
+            text_overlay.text = _("Paused");
+        else if (game.game_over)
+            text_overlay.text = _("Game Over");
+        else
+            text_overlay.text = null;
+    }
+}
+
+private class BlockActor : Clutter.Clone
+{
+    public Block block;
+
+    public BlockActor (Block block, Clutter.Actor texture)
+    {
+        Object (source: texture);
+        this.block = block;
+    }
+
+    public void explode ()
+    {
+        raise_top ();
+        var timeline = new Clutter.Timeline (720);
+        timeline.completed.connect (explode_complete_cb);
+        animate_with_timeline (Clutter.AnimationMode.EASE_OUT_QUINT, timeline, "opacity", 0, "scale-x", 2f, "scale-y", 2f);
+    }
+    
+    private void explode_complete_cb ()
+    {
+        destroy ();
+    }
+}
+
+private class TextOverlay : Clutter.CairoTexture
+{
+    private string? _text = null;
+    public string text
+    {
+        get { return _text; }
+        set { _text = value; invalidate (); }
+    }
+
+    public TextOverlay ()
+    {
+        auto_resize = true;
+    }
+
+    protected override bool draw (Cairo.Context cr)
+    {
+        clear ();
+
+        if (text == null)
+            return false;
+
+        /* Center coordinates */
+        uint w, h;
+        get_surface_size (out w, out h);
+        cr.translate (w / 2, h / 2);
+
+        var desc = Pango.FontDescription.from_string ("Sans");
+
+        var layout = Pango.cairo_create_layout (cr);
+        layout.set_text (text, -1);
+
+        var dummy_layout = layout.copy ();
+        dummy_layout.set_font_description (desc);
+        int lw, lh;
+        dummy_layout.get_size (out lw, out lh);
+
+        desc.set_absolute_size (((float) lh / lw) * Pango.SCALE * w * 0.7);
+        layout.set_font_description (desc);
+
+        layout.get_size (out lw, out lh);
+        cr.move_to (-((double)lw / Pango.SCALE) / 2, -((double)lh / Pango.SCALE) / 2);
+        Pango.cairo_layout_path (cr, layout);
+        cr.set_source_rgb (0.333333333333, 0.341176470588, 0.32549019607);
+
+        /* A linewidth of 2 pixels at the default size. */
+        cr.set_line_width (width / 100.0);
+        cr.stroke_preserve ();
+
+        cr.set_source_rgb (1.0, 1.0, 1.0);
+        cr.fill ();
+
+        return false;
+    }
+}
+
+public class BlockTexture : Clutter.CairoTexture
+{
+    private int color;
+    private string? _theme = null;
+    public string? theme
+    {
+        get { return _theme; }
+        set
+        {
+            if (_theme == value)
+                return;
+            _theme = value;
+            invalidate ();
+        }
+    }
+    
+    public BlockTexture (int color, int size)
+    {
+        auto_resize = true;
+        set_surface_size (size, size);
+        this.color = color.clamp (0, 6);
+    }
+
+    protected override bool draw (Cairo.Context cr)
+    {
+        clear ();
+
+        uint w, h;
+        get_surface_size (out w, out h);      
+        cr.scale (w, h);
+
+        switch (theme)
+        {
+        default:
+        case "plain":
+            draw_plain (cr);
+            break;
+        case "clean":
+            draw_clean (cr);
+            break;
+        case "tangoflat":
+            draw_tango (cr, false);
+            break;
+        case "tangoshaded":
+            draw_tango (cr, true);
+            break;
+        }
+
+        return false;
+    }
+
+    private void draw_plain (Cairo.Context cr)
+    {
+        const double colors[32] =
+        {
+            1.0, 0.0, 0.0,
+            0.0, 1.0, 0.0,
+            0.0, 0.0, 1.0,
+            1.0, 1.0, 1.0,
+            1.0, 1.0, 0.0,
+            1.0, 0.0, 1.0,
+            0.0, 1.0, 1.0
+        };
+
+        cr.set_source_rgb(colors[color * 3], colors[color * 3 + 1], colors[color * 3 + 2]);
+        cr.paint ();
+    }
+
+    private void draw_rounded_rectangle (Cairo.Context cr, double x, double y, double w, double h, double r)
+    {
+        cr.move_to (x + r, y);
+        cr.line_to (x + w - r, y);
+        cr.curve_to (x + w - (r/2), y, x + w, y + r / 2, x + w, y + r);
+        cr.line_to (x + w, y + h - r);
+        cr.curve_to (x + w, y + h - r / 2, x + w - r / 2, y + h, x + w - r, y + h);
+        cr.line_to (x + r, y + h);
+        cr.curve_to (x + r / 2, y + h, x, y + h - r / 2, x, y + h - r);
+        cr.line_to (x, y + r);
+        cr.curve_to (x, y + r / 2, x + r / 2, y, x + r, y);
+    }
+
+    private void draw_clean (Cairo.Context cr)
+    {
+        /* The colors, first the lighter then the darker fill (for the gradient)
+           and then the stroke color  */
+        const double colors[72] =
+        {
+            0.780392156863, 0.247058823529, 0.247058823529,
+            0.713725490196, 0.192156862745, 0.192156862745,
+            0.61568627451, 0.164705882353, 0.164705882353, /* red */
+    
+            0.552941176471, 0.788235294118, 0.32549019607,
+            0.474509803922, 0.713725490196, 0.243137254902,
+            0.388235294118, 0.596078431373, 0.18431372549, /* green */
+    
+            0.313725490196, 0.450980392157, 0.623529411765,
+            0.239215686275, 0.345098039216, 0.474509803922,
+            0.21568627451, 0.313725490196, 0.435294117647, /* blue */
+    
+            1.0, 1.0, 1.0,
+            0.909803921569, 0.909803921569, 0.898039215686,
+            0.701960784314, 0.701960784314, 0.670588235294, /* white */
+    
+            0.945098039216, 0.878431372549, 0.321568627451,
+            0.929411764706, 0.839215686275, 0.113725490196,
+            0.760784313725, 0.682352941176, 0.0274509803922, /* yellow */
+    
+            0.576470588235, 0.364705882353, 0.607843137255,
+            0.443137254902, 0.282352941176, 0.46666666666,
+            0.439215686275, 0.266666666667, 0.46666666666, /* purple */
+    
+            0.890196078431, 0.572549019608, 0.258823529412,
+            0.803921568627, 0.450980392157, 0.101960784314,
+            0.690196078431, 0.388235294118, 0.0901960784314, /* orange */
+    
+            0.392156862745, 0.392156862745, 0.392156862745,
+            0.262745098039, 0.262745098039, 0.262745098039,
+            0.21568627451, 0.235294117647, 0.23921568627 /* grey */
+        };
+
+        /* Layout the block */
+        draw_rounded_rectangle (cr, 0.05, 0.05, 0.9, 0.9, 0.05);
+
+        /* Draw outline */
+        cr.set_source_rgb (colors[color * 9 + 6], colors[color * 9 + 7], colors[color * 9 + 8]);
+        cr.set_line_width (0.1);
+        cr.stroke_preserve ();
+
+        /* Fill with gradient */
+        var pat = new Cairo.Pattern.linear (0.35, 0, 0.55, 0.9);
+        pat.add_color_stop_rgb (0.0, colors[color * 9], colors[color * 9 + 1], colors[color * 9 + 2]);
+        pat.add_color_stop_rgb (1.0, colors[color * 9 + 3], colors[color * 9 + 4], colors[color * 9 + 5]);
+        cr.set_source (pat);
+        cr.fill ();
+    }
+
+    private void draw_tango (Cairo.Context cr, bool use_gradients)
+    {
+        /* The following garbage is derived from the official tango style guide */
+        const double colors[72] =
+        {
+            0.93725490196078431, 0.16078431372549021, 0.16078431372549021,
+            0.8, 0.0, 0.0,
+            0.64313725490196083, 0.0, 0.0, /* red */
+
+            0.54117647058823526, 0.88627450980392153, 0.20392156862745098,
+            0.45098039215686275, 0.82352941176470584, 0.086274509803921567,
+            0.30588235294117649, 0.60392156862745094, 0.023529411764705882, /* green */
+
+            0.44705882352941179, 0.62352941176470589, 0.81176470588235294,
+            0.20392156862745098, 0.396078431372549, 0.64313725490196083,
+            0.12549019607843137, 0.29019607843137257, 0.52941176470588236, /* blue */
+
+            0.93333333333333335, 0.93333333333333335, 0.92549019607843142,
+            0.82745098039215681, 0.84313725490196079, 0.81176470588235294,
+            0.72941176470588232, 0.74117647058823533, 0.71372549019607845, /* white */
+
+            0.9882352941176471, 0.9137254901960784, 0.30980392156862746,
+            0.92941176470588238, 0.83137254901960789, 0.0,
+            0.7686274509803922, 0.62745098039215685, 0.0, /* yellow */
+
+            0.67843137254901964, 0.49803921568627452, 0.6588235294117647,
+            0.45882352941176469, 0.31372549019607843, 0.4823529411764706,
+            0.36078431372549019, 0.20784313725490197, 0.4, /* purple */
+
+            0.9882352941176471, 0.68627450980392157, 0.24313725490196078,
+            0.96078431372549022, 0.47450980392156861, 0.0,
+            0.80784313725490198, 0.36078431372549019, 0.0, /* orange (replacing cyan) */
+
+            0.33, 0.34, 0.32,
+            0.18, 0.2, 0.21,
+            0.10, 0.12, 0.13 /* grey */
+        };
+
+        if (use_gradients)
+        {
+             var pat = new Cairo.Pattern.linear (0.35, 0, 0.55, 0.9);
+             pat.add_color_stop_rgb (0.0, colors[color * 9], colors[color * 9 + 1], colors[color * 9 + 2]);
+             pat.add_color_stop_rgb (1.0, colors[color * 9 + 3], colors[color * 9 + 4], colors[color * 9 + 5]);
+             cr.set_source (pat);
+        }
+        else
+             cr.set_source_rgb (colors[color * 9], colors[color * 9 + 1], colors[color * 9 + 2]);
+
+        draw_rounded_rectangle (cr, 0.05, 0.05, 0.9, 0.9, 0.2);
+        cr.fill_preserve ();  /* fill with shaded gradient */
+
+        cr.set_source_rgb (colors[color * 9 + 6], colors[color * 9 + 7], colors[color * 9 + 8]);
+
+        /* Add darker outline */
+        cr.set_line_width (0.1);
+        cr.stroke ();
+
+        draw_rounded_rectangle (cr, 0.15, 0.15, 0.7, 0.7, 0.08);
+        if (use_gradients)
+        {
+            var pat = new Cairo.Pattern.linear (-0.3, -0.3, 0.8, 0.8);
+            /* yellow and white blocks need a brighter highlight */
+            switch (color)
+            {
+            case 3:
+            case 4:
+                pat.add_color_stop_rgba (0.0, 1.0, 1.0, 1.0, 1.0);
+                pat.add_color_stop_rgba (1.0, 1.0, 1.0, 1.0, 0.0);
+                break;
+            default:
+                pat.add_color_stop_rgba (0.0, 0.9295, 0.9295, 0.9295, 1.0);
+                pat.add_color_stop_rgba (1.0, 0.9295, 0.9295, 0.9295, 0.0);
+                break;
+            }
+            cr.set_source (pat);
+        }
+        else
+            cr.set_source_rgba (1.0, 1.0, 1.0, 0.35);
+
+        /* Add inner edge highlight */
+        cr.stroke ();
+    }   
+}
diff --git a/quadrapassel/src/game.vala b/quadrapassel/src/game.vala
new file mode 100644
index 0000000..d0a2d7b
--- /dev/null
+++ b/quadrapassel/src/game.vala
@@ -0,0 +1,745 @@
+const int NCOLORS = 7;
+
+private const int block_table[448] =
+{
+    /* *** */
+    /* *   */
+    0, 0, 0, 0,
+    1, 1, 1, 0,
+    1, 0, 0, 0,
+    0, 0, 0, 0,
+
+    0, 1, 0, 0,
+    0, 1, 0, 0,
+    0, 1, 1, 0,
+    0, 0, 0, 0,
+
+    0, 0, 1, 0,
+    1, 1, 1, 0,
+    0, 0, 0, 0,
+    0, 0, 0, 0,
+
+    1, 1, 0, 0,
+    0, 1, 0, 0,
+    0, 1, 0, 0,
+    0, 0, 0, 0,
+
+    /* *** */
+    /*   * */
+    0, 0, 0, 0,
+    1, 1, 1, 0,
+    0, 0, 1, 0,
+    0, 0, 0, 0,
+
+    0, 1, 1, 0,
+    0, 1, 0, 0,
+    0, 1, 0, 0,
+    0, 0, 0, 0,
+
+    1, 0, 0, 0,
+    1, 1, 1, 0,
+    0, 0, 0, 0,
+    0, 0, 0, 0,
+
+    0, 1, 0, 0,
+    0, 1, 0, 0,
+    1, 1, 0, 0,
+    0, 0, 0, 0,
+
+    /* *** */
+    /*  *  */
+    0, 0, 0, 0,
+    1, 1, 1, 0,
+    0, 1, 0, 0,
+    0, 0, 0, 0,
+
+    0, 1, 0, 0,
+    0, 1, 1, 0,
+    0, 1, 0, 0,
+    0, 0, 0, 0,
+
+    0, 1, 0, 0,
+    1, 1, 1, 0,
+    0, 0, 0, 0,
+    0, 0, 0, 0,
+
+    0, 1, 0, 0,
+    1, 1, 0, 0,
+    0, 1, 0, 0,
+    0, 0, 0, 0,
+
+    /*  ** */
+    /* **  */
+
+    0, 0, 0, 0,
+    0, 1, 1, 0,
+    1, 1, 0, 0,
+    0, 0, 0, 0,
+
+    0, 1, 0, 0,
+    0, 1, 1, 0,
+    0, 0, 1, 0,
+    0, 0, 0, 0,
+
+    0, 1, 1, 0,
+    1, 1, 0, 0,
+    0, 0, 0, 0,
+    0, 0, 0, 0,
+
+    1, 0, 0, 0,
+    1, 1, 0, 0,
+    0, 1, 0, 0,
+    0, 0, 0, 0,
+          
+    /* **  */
+    /*  ** */
+
+    0, 0, 0, 0,
+    1, 1, 0, 0,
+    0, 1, 1, 0,
+    0, 0, 0, 0,
+
+    0, 0, 1, 0,
+    0, 1, 1, 0,
+    0, 1, 0, 0,
+    0, 0, 0, 0,
+
+    1, 1, 0, 0,
+    0, 1, 1, 0,
+    0, 0, 0, 0,
+    0, 0, 0, 0,
+
+    0, 1, 0, 0,
+    1, 1, 0, 0,
+    1, 0, 0, 0,
+    0, 0, 0, 0,
+
+    /* **** */
+    0, 0, 0, 0,
+    1, 1, 1, 1,
+    0, 0, 0, 0,
+    0, 0, 0, 0,
+
+    0, 1, 0, 0,
+    0, 1, 0, 0,
+    0, 1, 0, 0,
+    0, 1, 0, 0,
+
+    0, 0, 0, 0,
+    1, 1, 1, 1,
+    0, 0, 0, 0,
+    0, 0, 0, 0,
+
+    0, 1, 0, 0,
+    0, 1, 0, 0,
+    0, 1, 0, 0,
+    0, 1, 0, 0,
+
+    /* ** */
+    /* ** */
+    0, 0, 0, 0,
+    0, 1, 1, 0,
+    0, 1, 1, 0,
+    0, 0, 0, 0,
+
+    0, 0, 0, 0,
+    0, 1, 1, 0,
+    0, 1, 1, 0,
+    0, 0, 0, 0,
+
+    0, 0, 0, 0,
+    0, 1, 1, 0,
+    0, 1, 1, 0,
+    0, 0, 0, 0,
+
+    0, 0, 0, 0,
+    0, 1, 1, 0,
+    0, 1, 1, 0,
+    0, 0, 0, 0
+};
+
+public class Block
+{
+    /* Location of block */
+    public int x;
+    public int y;
+
+    /* Color of block */
+    public int color;
+    
+    public Block copy ()
+    {
+        var b = new Block ();
+        b.x = x;
+        b.y = y;
+        b.color = color;
+        return b;
+    }
+}
+
+public class Shape
+{
+    /* Location of shape */
+    public int x;
+    public int y;
+
+    /* Rotation angle */
+    public int rotation;
+
+    /* Piece type */
+    public int type;
+
+    /* Blocks that make up this shape */
+    public List<Block> blocks = null;
+
+    public Shape copy ()
+    {
+        var s = new Shape ();
+        s.x = x;
+        s.y = y;
+        s.rotation = rotation;
+        s.type = type;
+        foreach (var b in blocks)
+            s.blocks.append (b.copy ());
+        return s;
+    }
+}
+
+public class Game : Object
+{
+    /* Falling shape */
+    public Shape? shape = null;
+
+    /* Next shape to be used */
+    public Shape? next_shape = null;
+
+    /* Placed blocks */
+    public Block[,] blocks;
+
+    public int width { get { return blocks.length[0]; } }
+    public int height { get { return blocks.length[1]; } }
+    
+    /* Number of lines that have been destroyed */
+    public int n_lines_destroyed = 0;
+
+    /* Game score */
+    public int score = 0;
+
+    /* Level play started on */
+    private int starting_level = 1;
+
+    /* true if should pick difficult blocks to place */
+    private bool pick_difficult_blocks = false;
+
+    /* The current level */
+    public int level { get { return starting_level + n_lines_destroyed / 10; } }
+    
+    /* true if we are in fast forward mode */
+    private bool fast_forward = false;
+
+    /* Timer to animate block drops */
+    private uint drop_timeout = 0;
+
+    /* true if the game has started */
+    private bool has_started = false;
+
+    /* true if games is paused */
+    private bool _paused = false;
+    public bool paused
+    {
+        get { return _paused; }
+        set
+        {
+            _paused = value;
+            if (has_started)
+                setup_drop_timer ();
+            pause_changed ();
+        }
+    }
+
+    public bool game_over = false;
+
+    public signal void started ();
+    public signal void shape_added ();
+    public signal void shape_moved ();
+    public signal void shape_dropped ();
+    public signal void shape_rotated ();
+    public signal void shape_landed (int[] lines, List<Block> line_blocks);
+    public signal void pause_changed ();
+    public signal void complete ();
+
+    public Game (int lines = 20, int columns = 14, int starting_level = 1, int filled_lines = 0, int fill_prob = 5, bool pick_difficult_blocks = false)
+    {
+        this.starting_level = starting_level;
+        this.pick_difficult_blocks = pick_difficult_blocks;
+
+        blocks = new Block[columns, lines];
+        /* Start with some pre-filled spaces */
+        for (var y = 0; y < height; y++)
+        {
+            /* Pick at least one column to be empty */
+            var blank = Random.int_range (0, width);
+
+            for (var x = 0; x < width; x++)
+            {
+                if (y >= (height - filled_lines) && x != blank && Random.int_range (0, 10) < fill_prob)
+                {
+                    blocks[x, y] = new Block ();
+                    blocks[x, y].x = x;
+                    blocks[x, y].y = y;
+                    blocks[x, y].color = Random.int_range (0, NCOLORS);
+                }
+                else
+                    blocks[x, y] = null;
+            }
+        }
+
+        if (!pick_difficult_blocks)
+            next_shape = pick_random_shape ();
+    }
+
+    public Game copy ()
+    {
+        var g = new Game ();
+        if (shape != null)
+            g.shape = shape.copy ();
+        if (next_shape != null)
+            g.next_shape = next_shape.copy ();
+        for (var x = 0; x < width; x++)
+        {
+            for (var y = 0; y < height; y++)
+            {
+                if (blocks[x, y] != null)
+                    g.blocks[x, y] = blocks[x, y].copy ();
+            }
+        }
+        g.n_lines_destroyed = n_lines_destroyed;
+        g.score = score;
+        g.starting_level = starting_level;
+        g.pick_difficult_blocks = pick_difficult_blocks;
+        g.fast_forward = fast_forward;
+        g.has_started = has_started;
+        g._paused = _paused;
+        g.game_over = game_over;
+
+        return g;
+    }
+
+    public void start ()
+    {
+        has_started = true;
+        add_shape ();
+        setup_drop_timer ();
+        started ();
+    }
+
+    public bool move_left ()
+    {
+        return move_shape (-1, 0, 0);
+    }
+
+    public bool move_right ()
+    {
+        return move_shape (1, 0, 0);
+    }
+
+    public bool rotate_left ()
+    {
+        return move_shape (0, 0, -1);
+    }
+
+    public bool rotate_right ()
+    {
+        return move_shape (0, 0, 1);
+    }
+
+    public void set_fast_forward (bool enable)
+    {
+        fast_forward = enable;
+        setup_drop_timer ();
+    }
+
+    public void drop ()
+    {
+        if (shape == null)
+            return;
+
+        while (move_shape (0, 1, 0));
+        fall_timeout_cb ();
+    }
+
+    public void stop ()
+    {
+        if (drop_timeout != 0)
+            Source.remove (drop_timeout);        
+    }
+
+    private void setup_drop_timer ()
+    {
+        var timestep = (int) Math.round (80 + 800.0 * Math.pow (0.75, level - 1));
+        timestep = int.max (10, timestep);
+
+        /* In fast forward mode drop at the fastest rate */
+        if (fast_forward)
+            timestep = 80;
+
+        if (drop_timeout != 0)
+            Source.remove (drop_timeout);
+        drop_timeout = 0;
+        if (!paused)
+            drop_timeout = Timeout.add (timestep, fall_timeout_cb);
+    }
+
+    private bool fall_timeout_cb ()
+    {
+        /* Drop the shape down, and create a new one when it can't move */
+        if (!move_shape (0, 1, 0))
+        {
+            /* Destroy any lines created */
+            land_shape ();
+
+            /* Add a new shape */
+            add_shape ();
+        }
+
+        return true;
+    }
+
+    private void add_shape ()
+    {
+        if (pick_difficult_blocks)
+            shape = pick_difficult_shape ();
+        else
+        {
+            shape = (owned) next_shape;
+            next_shape = pick_random_shape ();
+        }
+
+        foreach (var b in shape.blocks)
+        {
+            var x = shape.x + b.x;
+            var y = shape.y + b.y;
+
+            /* Abort if can't place there */
+            if (y >= 0 && blocks[x, y] != null)
+            {
+                // FIXME: Place it where it can fit
+
+                if (drop_timeout != 0)
+                    Source.remove (drop_timeout);
+                drop_timeout = 0;
+                shape = null;
+                game_over = true;
+                complete ();
+                return;
+            }
+        }
+
+        shape_added ();
+    }
+
+    private Shape pick_random_shape ()
+    {
+        return make_shape (Random.int_range (0, NCOLORS), Random.int_range (0, 4));
+    }
+
+    private Shape pick_difficult_shape ()
+    {
+        var metrics = new int[NCOLORS];
+        for (var type = 0; type < NCOLORS; type++)
+        {
+            metrics[type] = -32000;
+            for (var rotation = 0; rotation < 4; rotation++)
+            {
+                for (var pos = 0; pos < width; pos++)
+                {
+                    /* Copy the current game and create a block of the given type */
+                    var g = copy ();
+                    g.pick_difficult_blocks = false;
+                    g.shape = make_shape (type, rotation);
+
+                    /* Move tile to position from the left */
+                    var valid_position = true;
+                    while (g.move_left ());
+                    for (var x = 0; x < pos; x++)
+                    {
+                        if (!g.move_right ())
+                        {
+                            valid_position = false;
+                            break;
+                        }
+                    }
+
+                    if (!valid_position)
+                        break;
+
+                    /* Drop the tile here and check the metric */
+                    var orig_lines = g.n_lines_destroyed;
+                    g.drop ();
+
+                    /* High metric for each line destroyed */
+                    var metric = (g.n_lines_destroyed - orig_lines) * 5000;
+                    
+                    /* Low metric for large columns */
+                    for (var x = 0; x < width; x++)
+                    {
+                        int y;
+                        for (y = 0; y < height; y++)
+                        {
+                            if (g.blocks[x, y] != null)
+                                break;
+                        }
+
+                        metric -= 5 * (height - y);
+                    }
+                    
+                    if (metric > metrics[type])
+                        metrics[type] = metric;
+
+                    /* Destroy this copy */
+                    g.stop ();
+                }
+            }
+        }
+
+        /* Perturb score (-2 to +2), to avoid stupid tie handling */
+        for (var i = 0; i < NCOLORS; i++)
+            metrics[i] += Random.int_range (-2, 2);
+
+        /* Sorts possible_types by priorities, worst (interesting to us) first */
+        var possible_types = new int[NCOLORS];
+        for (var i = 0; i < NCOLORS; i++)
+            possible_types[i] = i;
+        for (var i = 0; i < NCOLORS; i++)
+        {
+            for (var j = 0; j < NCOLORS - 1; j++)
+            {
+                if (metrics[possible_types[j]] > metrics[possible_types[j + 1]])
+                {
+                    int t = possible_types[j];
+                    possible_types[j] = possible_types[j + 1];
+                    possible_types[j + 1] = t;
+                }
+            }
+        }
+
+        /* Actually choose a piece */
+        var rnd = Random.int_range (0, 99);
+        if (rnd < 75)
+            return make_shape (possible_types[0], Random.int_range (0, 4));
+        else if (rnd < 92)
+            return make_shape (possible_types[1], Random.int_range (0, 4));
+        else if (rnd < 98)
+            return make_shape (possible_types[2], Random.int_range (0, 4));
+        else
+            return make_shape (possible_types[3], Random.int_range (0, 4));
+    }
+
+    private Shape make_shape (int type, int rotation)
+    {
+        var shape = new Shape ();
+        shape.type = type;
+        shape.rotation = rotation;
+
+        /* Place this block at top of the field */
+        var offset = shape.type * 64 + shape.rotation * 16;
+        var min_width = 4, max_width = 0, min_height = 4, max_height = 0;
+        for (var x = 0; x < 4; x++)
+        {
+            for (var y = 0; y < 4; y++)
+            {
+                if (block_table[offset + y * 4 + x] == 0)
+                    continue;
+
+                min_width = int.min (x, min_width);
+                max_width = int.max (x + 1, max_width);
+                min_height = int.min (y, min_height);
+                max_height = int.max (y + 1, max_height);
+
+                var b = new Block ();
+                b.color = shape.type;
+                b.x = x;
+                b.y = y;
+                shape.blocks.append (b);
+            }
+        }
+        var block_width = max_width - min_width;
+        shape.x = (width - block_width) / 2 - min_width;
+        shape.y = -min_height;
+
+        return shape;
+    }
+
+    private void land_shape ()
+    {
+        /* Leave these blocks here */
+        foreach (var b in shape.blocks)
+        {
+            b.x += shape.x;
+            b.y += shape.y;
+            blocks[b.x, b.y] = b;
+        }
+
+        var fall_distance = 0;
+        var lines = new int[4];
+        var n_lines = 0;
+        var base_line_destroyed = false;
+        for (var y = height - 1; y >= 0; y--)
+        {
+            var explode = true;
+            for (var x = 0; x < width; x++)
+            {
+                if (blocks[x, y] == null)
+                {
+                    explode = false;
+                    break;
+                }
+            }
+            
+            if (explode)
+            {
+                if (y == height - 1)
+                    base_line_destroyed = true;
+                lines[n_lines] = y;
+                n_lines++;
+            }
+        }
+        lines.resize (n_lines);
+
+        List<Block> line_blocks = null;
+        for (var y = height - 1; y >= 0; y--)
+        {
+            var explode = true;
+            for (var x = 0; x < width; x++)
+            {
+                if (blocks[x, y] == null)
+                {
+                    explode = false;
+                    break;
+                }
+            }
+
+            if (explode)
+            {
+                for (var x = 0; x < width; x++)
+                {
+                    line_blocks.append (blocks[x, y]);
+                    blocks[x, y] = null;
+                }
+                fall_distance++;
+            }
+            else if (fall_distance > 0)
+            {
+                for (var x = 0; x < width; x++)
+                {
+                    var b = blocks[x, y];
+                    if (b != null)
+                    {
+                        b.y += fall_distance;
+                        blocks[b.x, b.y] = b;
+                        blocks[x, y] = null;
+                    }
+                }
+            }
+        }
+
+        var old_level = level;
+
+        /* Score points */
+        n_lines_destroyed += n_lines;
+        switch (n_lines)
+        {
+        case 0:
+            break;
+        case 1:
+            score += 40 * level;
+            break;
+        case 2:
+            score += 100 * level;
+            break;
+        case 3:
+            score += 300 * level;
+            break;
+        case 4:
+            score += 1200 * level;
+            break;
+        }
+        /* You get a bonus for getting back to the base */
+        if (base_line_destroyed)
+            score += 10000 * level;
+
+        /* Increase speed if level has changed */
+        if (level != old_level)
+            setup_drop_timer ();
+
+        shape_landed (lines, line_blocks);
+        shape = null;
+    }
+
+    private bool move_shape (int x_step, int y_step, int r_step)
+    {
+        if (shape == null)
+            return false;
+
+        /* Check it can fit into the new location */
+        rotate_shape (r_step);
+        var can_move = true;
+        foreach (var b in shape.blocks)
+        {
+            var x = shape.x + x_step + b.x;
+            var y = shape.y + y_step + b.y;
+            if (x < 0 || x >= width || y >= height || blocks[x, y] != null)
+            {
+                can_move = false;
+                break;
+            }
+        }
+
+        /* Place in the new location or put it back where it was */
+        if (can_move)
+        {
+            shape.x += x_step;
+            shape.y += y_step;
+
+            if (x_step != 0)
+                shape_moved ();
+            else if (y_step > 0)
+                shape_dropped ();
+            else
+                shape_rotated ();
+        }
+        else
+            rotate_shape (-r_step);
+
+        return can_move;
+    }
+
+    private void rotate_shape (int r_step)
+    {
+        var r = shape.rotation + r_step;
+        if (r < 0)
+            r += 4;
+        if (r >= 4)
+            r -= 4;
+
+        if (r == shape.rotation)
+            return;
+        shape.rotation = r;
+
+        /* Rearrange current blocks */
+        unowned List<Block> b = shape.blocks;
+        var offset = shape.type * 64 + r * 16;
+        for (var x = 0; x < 4; x++)
+        {
+            for (var y = 0; y < 4; y++)
+            {
+                if (block_table[offset + y * 4 + x] != 0)
+                {
+                    b.data.x = x;
+                    b.data.y = y;
+                    b = b.next;
+                }
+            }
+        }
+    }
+}
diff --git a/quadrapassel/src/preview.vala b/quadrapassel/src/preview.vala
new file mode 100644
index 0000000..5fcd81e
--- /dev/null
+++ b/quadrapassel/src/preview.vala
@@ -0,0 +1,111 @@
+public class Preview : GtkClutter.Embed
+{
+    /* Textures used to draw blocks */
+    private BlockTexture[] block_textures;
+
+    /* Clutter representation of a piece */
+    private Clutter.Group? piece = null;
+
+    public string theme
+    {
+        set
+        {
+            foreach (var texture in block_textures)
+                texture.theme = value;
+            update_block ();
+        }
+    }
+
+    private int cell_size
+    {
+        get { return (get_allocated_width () + get_allocated_height ()) / 2 / 5; }
+    }
+    
+    private Game? _game = null;
+    public Game? game
+    {
+        get { return _game; }
+        set
+        {
+            if (_game != null)
+                SignalHandler.disconnect_matched (_game, SignalMatchType.DATA, 0, 0, null, null, this);
+            _game = value;
+            _game.shape_added.connect (shape_added_cb);
+            update_block ();
+        }
+    }
+    
+    private bool _enabled = true;
+    public bool enabled
+    {
+        get { return _enabled; }
+        set { _enabled = value; update_block (); }
+    }
+
+    public Preview ()
+    {
+        size_allocate.connect (size_allocate_cb);
+
+        /* FIXME: We should scale with the rest of the UI, but that requires
+         * changes to the widget layout - i.e. wrap the preview in an
+         * fixed-aspect box. */
+        set_size_request (120, 120);
+        var stage = (Clutter.Stage) get_stage ();
+
+        Clutter.Color stage_color = { 0x0, 0x0, 0x0, 0xff };
+        stage.set_color (stage_color);
+
+        block_textures = new BlockTexture[NCOLORS];
+        for (var i = 0; i < block_textures.length; i++)
+        {
+            // FIXME: Have to set a size to avoid an assertion in Clutter
+            block_textures[i] = new BlockTexture (i, 1);
+            block_textures[i].hide ();
+            stage.add_actor (block_textures[i]);
+        }
+    }
+
+    private void shape_added_cb ()
+    {
+        update_block ();
+    }
+
+    private void update_block ()
+    {
+        if (piece != null)
+            piece.destroy ();
+
+        if (game == null || game.next_shape == null || !enabled)
+            return;
+
+        piece = new Clutter.Group ();
+        var stage = (Clutter.Stage) get_stage ();
+        stage.add_actor (piece);
+
+        var min_width = 4, max_width = 0, min_height = 4, max_height = 0;
+        foreach (var b in game.next_shape.blocks)
+        {
+            min_width = int.min (b.x, min_width);
+            max_width = int.max (b.x + 1, max_width);
+            min_height = int.min (b.y, min_height);
+            max_height = int.max (b.y + 1, max_height);
+
+            var a = new Clutter.Clone (block_textures[b.color]);
+            a.set_size (cell_size, cell_size);
+            a.set_position (b.x * cell_size, b.y * cell_size);
+            piece.add_actor (a);
+        }
+
+        piece.set_anchor_point ((min_width + max_width) * 0.5f * cell_size, (min_height + max_height) * 0.5f * cell_size);
+        piece.set_position (get_allocated_width () / 2, get_allocated_height () / 2);
+        piece.set_scale (0.6, 0.6);
+        piece.animate (Clutter.AnimationMode.EASE_IN_OUT_SINE, 180, "scale-x", 1.0, "scale-y", 1.0);
+    }
+
+    private void size_allocate_cb (Gtk.Allocation allocation)
+    {
+        foreach (var texture in block_textures)
+            texture.set_size (cell_size, cell_size);
+        update_block ();
+    }
+}
diff --git a/quadrapassel/src/quadrapassel.vala b/quadrapassel/src/quadrapassel.vala
new file mode 100644
index 0000000..824b95f
--- /dev/null
+++ b/quadrapassel/src/quadrapassel.vala
@@ -0,0 +1,696 @@
+public class Quadrapassel
+{
+    /* Application settings */
+    private Settings settings;
+
+    /* Main window */
+    private Gtk.Window main_window;
+
+    /* Game being played */
+    private Game? game = null;
+
+    /* Rendering of game */
+    private GameView view;
+
+    /* Preview of the next shape */
+    private Preview preview;
+
+    /* Label showing current score */
+    private Gtk.Label score_label;
+
+    /* Label showing the number of lines destroyed */
+    private Gtk.Label n_destroyed_label;
+
+    /* Label showing the current level */
+    private Gtk.Label level_label;
+
+    private GnomeGamesSupport.Scores high_scores;
+
+    private GnomeGamesSupport.PauseAction pause_action;
+
+    private Gtk.Dialog preferences_dialog;
+    private Gtk.SpinButton starting_level_spin;
+    private Preview theme_preview;
+    private Gtk.SpinButton fill_height_spinner;
+    private Gtk.SpinButton fill_prob_spinner;
+    private Gtk.CheckButton do_preview_toggle;
+    private Gtk.CheckButton difficult_blocks_toggle;
+    private Gtk.CheckButton rotate_counter_clock_wise_toggle;
+    private Gtk.CheckButton use_target_toggle;
+    private Gtk.CheckButton sound_toggle;
+
+    private const Gtk.ActionEntry actions[] =
+    {
+        { "GameMenu", null, N_("_Game") },
+        { "SettingsMenu", null, N_("_Settings") },
+        { "HelpMenu", null, N_("_Help") },
+        { "NewGame", GnomeGamesSupport.STOCK_NEW_GAME, null, null, null, new_game_cb },
+        { "Scores", GnomeGamesSupport.STOCK_SCORES, null, null, null, scores_cb },
+        { "Quit", Gtk.Stock.QUIT, null, null, null, quit_cb },
+        { "Preferences", Gtk.Stock.PREFERENCES, null, null, null, preferences_cb },
+        { "Contents", GnomeGamesSupport.STOCK_CONTENTS, null, null, null, help_cb },
+        { "About", Gtk.Stock.ABOUT, null, null, null, about_cb }
+    };
+
+    public Quadrapassel ()
+    {
+        var ui_description =
+        "<ui>" +
+        "  <menubar name='MainMenu'>" +
+        "    <menu action='GameMenu'>" +
+        "      <menuitem action='NewGame'/>" +
+        "      <menuitem action='_pause'/>" +
+        "      <separator/>" +
+        "      <menuitem action='Scores'/>" +
+        "      <separator/>" +
+        "      <menuitem action='Quit'/>" +
+        "    </menu>" +
+        "    <menu action='SettingsMenu'>" +
+        "      <menuitem action='Preferences'/>" +
+        "    </menu>" +
+        "    <menu action='HelpMenu'>" +
+        "      <menuitem action='Contents'/>" +
+        "      <menuitem action='About'/>" +
+        "    </menu>" +
+        "  </menubar>" +
+        "</ui>";
+
+        settings = new Settings ("org.gnome.quadrapassel");
+
+        main_window = new Gtk.Window (Gtk.WindowType.TOPLEVEL);
+        main_window.set_title (_("Quadrapassel"));
+
+        main_window.delete_event.connect (window_delete_event_cb);
+
+        main_window.set_default_size (500, 550);
+        //games_conf_add_window (main_window, KEY_SAVED_GROUP);
+
+        view = new GameView ();
+        view.theme = settings.get_string ("theme");
+        view.mute = !settings.get_boolean ("sound");
+
+        preview = new Preview ();
+        preview.theme = settings.get_string ("theme");
+        preview.enabled = settings.get_boolean ("do-preview");
+
+        /* prepare menus */
+        GnomeGamesSupport.stock_init ();
+        var action_group = new Gtk.ActionGroup ("MenuActions");
+        action_group.set_translation_domain (GETTEXT_PACKAGE);
+        action_group.add_actions (actions, this);
+        var ui_manager = new Gtk.UIManager ();
+        ui_manager.insert_action_group (action_group, 0);
+        try
+        {
+            ui_manager.add_ui_from_string (ui_description, -1);
+        }
+        catch (Error e)
+        {
+        }
+        main_window.add_accel_group (ui_manager.get_accel_group ());
+
+        pause_action = new GnomeGamesSupport.PauseAction ("_pause");
+        pause_action.state_changed.connect (pause_cb);
+        action_group.add_action_with_accel (pause_action, null);
+
+        var menubar = ui_manager.get_widget ("/MainMenu");
+
+        var hb = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
+
+        var vbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
+        main_window.add (vbox);
+        vbox.pack_start (menubar, false, false, 0);
+        vbox.pack_start (hb, true, true, 0);
+
+        main_window.set_events (main_window.get_events () | Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK);
+
+        var vb1 = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
+        vb1.set_border_width (10);
+        vb1.pack_start (view, true, true, 0);
+        hb.pack_start (vb1, true, true, 0);
+
+        main_window.key_press_event.connect (key_press_event_cb);
+        main_window.key_release_event.connect (key_release_event_cb);
+
+        var vb2 = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
+        vb2.set_border_width (10);
+        hb.pack_end (vb2, false, false, 0);
+
+        vb2.pack_start (preview, false, false, 0);
+
+        var score_grid = new Gtk.Grid ();
+
+        var label = new Gtk.Label (_("Score:"));
+        label.set_alignment (0.0f, 0.5f);
+        score_grid.attach (label, 0, 0, 1, 1);
+        score_label = new Gtk.Label ("0");
+        score_label.set_alignment (1.0f, 0.5f);
+        score_grid.attach (score_label, 1, 0, 1, 1);
+
+        label = new Gtk.Label (_("Lines:"));
+        label.set_alignment (0.0f, 0.5f);
+        score_grid.attach (label, 0, 1, 1, 1);
+        n_destroyed_label = new Gtk.Label ("0");
+        n_destroyed_label.set_alignment (1.0f, 0.5f);
+        score_grid.attach (n_destroyed_label, 1, 1, 1, 1);
+
+        label = new Gtk.Label (_("Level:"));
+        label.set_alignment (0.0f, 0.5f);
+        score_grid.attach (label, 0, 2, 1, 1);
+        level_label = new Gtk.Label ("0");
+        level_label.set_alignment (1.0f, 0.5f);
+        score_grid.attach (level_label, 1, 2, 1, 1);
+
+        vb2.pack_end (score_grid, true, false, 0);
+
+        high_scores = new GnomeGamesSupport.Scores ("quadrapassel",
+                                                    new GnomeGamesSupport.ScoresCategory[0],
+                                                    null,
+                                                    null,
+                                                    0,
+                                                    GnomeGamesSupport.ScoreStyle.PLAIN_DESCENDING);
+
+        pause_action.sensitive = false;
+    }
+
+    public void show ()
+    {
+        main_window.show_all ();
+    }
+
+    private void preferences_dialog_close_cb ()
+    {
+        preferences_dialog.destroy ();
+        preferences_dialog = null;
+    }
+
+    private void preferences_dialog_response_cb (int response_id)
+    {
+        preferences_dialog_close_cb ();
+    }
+
+    private void preferences_cb (Gtk.Action action)
+    {
+        if (preferences_dialog != null)
+        {
+            preferences_dialog.present ();
+            return;
+        }
+
+        preferences_dialog = new Gtk.Dialog.with_buttons (_("Quadrapassel Preferences"), main_window, (Gtk.DialogFlags)0, Gtk.Stock.CLOSE, Gtk.ResponseType.CLOSE, null);
+        preferences_dialog.set_border_width (5);
+        var vbox = (Gtk.Box) preferences_dialog.get_content_area ();
+        vbox.set_spacing (2);
+        preferences_dialog.close.connect (preferences_dialog_close_cb);
+        preferences_dialog.response.connect (preferences_dialog_response_cb);
+
+        var notebook = new Gtk.Notebook ();
+        notebook.set_border_width (5);
+        vbox.pack_start (notebook, true, true, 0);
+
+        vbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 18);
+        vbox.set_border_width (12);
+        var label = new Gtk.Label (_("Game"));
+        notebook.append_page (vbox, label);
+
+        var frame = new GnomeGamesSupport.Frame (_("Setup"));
+        var grid = new Gtk.Grid ();
+        grid.set_row_spacing (6);
+        grid.set_column_spacing (12);
+
+        /* pre-filled rows */
+        label = new Gtk.Label.with_mnemonic (_("_Number of pre-filled rows:"));
+        label.set_alignment (0, 0.5f);
+        label.set_hexpand (true);
+        grid.attach (label, 0, 0, 1, 1);
+
+        var adj = new Gtk.Adjustment (settings.get_int ("line-fill-height"), 0, game.height - 1, 1, 5, 0);
+        fill_height_spinner = new Gtk.SpinButton (adj, 10, 0);
+        fill_height_spinner.set_update_policy (Gtk.SpinButtonUpdatePolicy.ALWAYS);
+        fill_height_spinner.set_snap_to_ticks (true);
+        fill_height_spinner.value_changed.connect (fill_height_spinner_value_changed_cb);
+        grid.attach (fill_height_spinner, 1, 0, 2, 1);
+        label.set_mnemonic_widget (fill_height_spinner);
+
+        /* pre-filled rows density */
+        label = new Gtk.Label.with_mnemonic (_("_Density of blocks in a pre-filled row:"));
+        label.set_alignment (0, 0.5f);
+        label.set_hexpand (true);
+        grid.attach (label, 0, 1, 1, 1);
+
+        adj = new Gtk.Adjustment (settings.get_int ("line-fill-probability"), 0, 10, 1, 5, 0);
+        fill_prob_spinner = new Gtk.SpinButton (adj, 10, 0);
+        fill_prob_spinner.set_update_policy (Gtk.SpinButtonUpdatePolicy.ALWAYS);
+        fill_prob_spinner.set_snap_to_ticks (true);
+        fill_prob_spinner.value_changed.connect (fill_prob_spinner_value_changed_cb);
+        grid.attach (fill_prob_spinner, 1, 1, 1, 1);
+        label.set_mnemonic_widget (fill_prob_spinner);
+
+        /* starting level */
+        label = new Gtk.Label.with_mnemonic (_("_Starting level:"));
+        label.set_alignment (0, 0.5f);
+        label.set_hexpand (true);
+        grid.attach (label, 0, 2, 1, 1);
+
+        adj = new Gtk.Adjustment (settings.get_int ("starting-level"), 1, 20, 1, 5, 0);
+        starting_level_spin = new Gtk.SpinButton (adj, 10.0, 0);
+        starting_level_spin.set_update_policy (Gtk.SpinButtonUpdatePolicy.ALWAYS);
+        starting_level_spin.set_snap_to_ticks (true);
+        starting_level_spin.value_changed.connect (starting_level_value_changed_cb);
+        grid.attach (starting_level_spin, 1, 2, 1, 1);
+        label.set_mnemonic_widget (starting_level_spin);
+
+        frame.add (grid);
+        vbox.pack_start (frame, false, false, 0);
+
+        frame = new GnomeGamesSupport.Frame (_("Operation"));
+        var fvbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 6);
+
+        sound_toggle = new Gtk.CheckButton.with_mnemonic (_("_Enable sounds"));
+        sound_toggle.set_active (settings.get_boolean ("sound"));
+        sound_toggle.toggled.connect (sound_toggle_toggled_cb);
+        fvbox.pack_start (sound_toggle, false, false, 0);
+
+        do_preview_toggle = new Gtk.CheckButton.with_mnemonic (_("_Preview next block"));
+        do_preview_toggle.set_active (settings.get_boolean ("do-preview"));
+        do_preview_toggle.toggled.connect (do_preview_toggle_toggled_cb);
+        fvbox.pack_start (do_preview_toggle, false, false, 0);
+
+        difficult_blocks_toggle = new Gtk.CheckButton.with_mnemonic (_("Choose difficult _blocks"));
+        difficult_blocks_toggle.set_active (settings.get_boolean ("pick-difficult-blocks"));
+        difficult_blocks_toggle.toggled.connect (difficult_blocks_toggled_cb);
+        fvbox.pack_start (difficult_blocks_toggle, false, false, 0);
+
+        /* rotate counter clock wise */
+        rotate_counter_clock_wise_toggle = new Gtk.CheckButton.with_mnemonic (_("_Rotate blocks counterclockwise"));
+        rotate_counter_clock_wise_toggle.set_active (settings.get_boolean ("rotate-counter-clock-wise"));
+        rotate_counter_clock_wise_toggle.toggled.connect (set_rotate_counter_clock_wise);
+        fvbox.pack_start (rotate_counter_clock_wise_toggle, false, false, 0);
+
+        use_target_toggle = new Gtk.CheckButton.with_mnemonic (_("Show _where the block will land"));
+        fvbox.pack_start (use_target_toggle, false, false, 0);
+
+        frame.add (fvbox);
+        vbox.pack_start (frame, false, false, 0);
+
+        frame = new GnomeGamesSupport.Frame (_("Theme"));
+        grid = new Gtk.Grid ();
+        grid.set_border_width (0);
+        grid.set_row_spacing (6);
+        grid.set_column_spacing (12);
+
+        /* controls page */
+        vbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
+        vbox.set_border_width (12);
+        label = new Gtk.Label (_("Controls"));
+        notebook.append_page (vbox, label);
+
+        frame = new GnomeGamesSupport.Frame (_("Keyboard Controls"));
+        vbox.pack_start (frame, true, true, 0);
+
+        fvbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 6);
+        frame.add (fvbox);
+
+        var controls_list = new GnomeGamesSupport.ControlsList (settings);
+        controls_list.add_controls ("key-left", _("Move left"), 0,
+                                    "key-right", _("Move right"), 0,
+                                    "key-down", _("Move down"), 0,
+                                    "key-drop", _("Drop"), 0,
+                                    "key-rotate", _("Rotate"), 0,
+                                    "key-pause", _("_pause"), 0,
+                                    null);
+
+        fvbox.pack_start (controls_list, true, true, 0);
+
+        /* theme page */
+        vbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
+        vbox.set_border_width (12);
+        label = new Gtk.Label (_("Theme"));
+        notebook.append_page (vbox, label);
+
+        frame = new GnomeGamesSupport.Frame (_("Block Style"));
+        vbox.pack_start (frame, true, true, 0);
+
+        fvbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 6);
+        frame.add (fvbox);
+
+        var theme_combo = new Gtk.ComboBox ();
+        var theme_store = new Gtk.ListStore (2, typeof (string), typeof (string));
+        theme_combo.model = theme_store;
+        var renderer = new Gtk.CellRendererText ();
+        theme_combo.pack_start (renderer, true);
+        theme_combo.add_attribute (renderer, "text", 0);
+
+        Gtk.TreeIter iter;
+
+        theme_store.append (out iter);
+        theme_store.set (iter, 0, _("Plain"), 1, "plain", -1);
+        if (settings.get_string ("theme") == "plain")
+            theme_combo.set_active_iter (iter);
+
+        theme_store.append (out iter);
+        theme_store.set (iter, 0, _("Tango Flat"), 1, "tangoflat", -1);
+        if (settings.get_string ("theme") == "tangoflat")
+            theme_combo.set_active_iter (iter);
+
+        theme_store.append (out iter);
+        theme_store.set (iter, 0, _("Tango Shaded"), 1, "tangoshaded", -1);
+        if (settings.get_string ("theme") == "tangoshaded")
+            theme_combo.set_active_iter (iter);
+
+        theme_store.append (out iter);
+        theme_store.set (iter, 0, _("Clean"), 1, "clean", -1);
+        if (settings.get_string ("theme") == "clean")
+            theme_combo.set_active_iter (iter);
+
+        theme_combo.changed.connect (theme_combo_changed_cb);
+        fvbox.pack_start (theme_combo, false, false, 0);
+
+        theme_preview = new Preview ();
+        theme_preview.game = new Game ();
+        theme_preview.theme = settings.get_string ("theme");
+        fvbox.pack_start (theme_preview, true, true, 0);
+
+        preferences_dialog.show_all ();
+    }
+
+    private void sound_toggle_toggled_cb ()
+    {
+        var play_sound = sound_toggle.get_active ();
+        settings.set_boolean ("sound", play_sound);
+        view.mute = !play_sound;
+    }
+
+    private void do_preview_toggle_toggled_cb ()
+    {
+        var do_preview = do_preview_toggle.get_active ();
+        settings.set_boolean ("do-preview", do_preview);
+        preview.enabled = do_preview;
+    }
+
+    private void difficult_blocks_toggled_cb ()
+    {
+        settings.set_boolean ("pick-difficult-blocks", difficult_blocks_toggle.get_active ());
+    }
+
+    private void set_rotate_counter_clock_wise ()
+    {
+        settings.set_boolean ("rotate-counter-clock-wise", rotate_counter_clock_wise_toggle.get_active ());
+    }
+
+    private void theme_combo_changed_cb (Gtk.ComboBox widget)
+    {
+        Gtk.TreeIter iter;
+        widget.get_active_iter (out iter);
+        string theme;
+        widget.model.get (iter, 1, out theme);
+        view.theme = theme;
+        preview.theme = theme;
+        if (theme_preview != null)
+            theme_preview.theme = theme;
+        settings.set_string ("theme", theme);
+    }
+
+    private void fill_height_spinner_value_changed_cb (Gtk.SpinButton spin)
+    {
+        int value = spin.get_value_as_int ();
+        settings.set_int ("line-fill-height", value);
+    }
+
+    private void fill_prob_spinner_value_changed_cb (Gtk.SpinButton spin)
+    {
+        int value = spin.get_value_as_int ();
+        settings.set_int ("line-fill-probability", value);
+    }
+
+    private void starting_level_value_changed_cb (Gtk.SpinButton spin)
+    {
+        int value = spin.get_value_as_int ();
+        settings.set_int ("starting-level", value);
+    }
+
+    private void pause_cb ()
+    {
+        if (game != null)
+            game.paused = pause_action.get_is_paused ();
+    }
+
+    private bool window_delete_event_cb (Gtk.Widget window, Gdk.EventAny event)
+    {
+        quit ();
+        return true;
+    }
+
+    private void quit_cb (Gtk.Action action)
+    {
+        quit ();
+    }
+
+    private void quit ()
+    {
+        /* Record the score if the game isn't over. */
+        if (game != null && game.score > 0)
+            high_scores.add_plain_score (game.score);
+
+        Gtk.main_quit ();
+    }
+
+    private bool key_press_event_cb (Gtk.Widget widget, Gdk.EventKey event)
+    {
+        var keyval = upper_key (event.keyval);
+
+        if (game == null)
+            return false;
+
+        if (keyval == upper_key (settings.get_int ("key-pause")))
+        {
+            pause_action.set_is_paused (!pause_action.get_is_paused ());
+            return true;
+        }
+
+        if (game.paused)
+            return false;
+
+        if (keyval == upper_key (settings.get_int ("key-left")))
+        {
+            game.move_left ();
+            return true;
+        }
+        else if (keyval == upper_key (settings.get_int ("key-right")))
+        {
+            game.move_right ();
+            return true;
+        }
+        else if (keyval == upper_key (settings.get_int ("key-rotate")))
+        {
+            if (settings.get_boolean ("rotate-counter-clock-wise"))
+                game.rotate_left ();
+            else
+                game.rotate_right ();
+            return true;
+        }
+        else if (keyval == upper_key (settings.get_int ("key-down")))
+        {
+            game.set_fast_forward (true);
+            return true;
+        }
+        else if (keyval == upper_key (settings.get_int ("key-drop")))
+        {
+            game.drop ();
+            return true;
+        }
+
+        return false;
+    }
+
+    private bool key_release_event_cb (Gtk.Widget widget, Gdk.EventKey event)
+    {
+        var keyval = upper_key (event.keyval);
+
+        if (game == null)
+            return false;
+
+        if (keyval == upper_key (settings.get_int ("key-down")))
+        {
+            game.set_fast_forward (false);
+            return true;
+        }
+
+        return false;
+    }
+
+    private uint upper_key (uint keyval)
+    {
+        if (keyval > 255)
+            return keyval;
+        return ((char) keyval).toupper ();
+    }
+
+    private void new_game_cb (Gtk.Action action)
+    {
+        new_game ();
+    }
+    
+    private void new_game ()
+    {
+        if (game != null)
+        {
+            game.stop ();
+            SignalHandler.disconnect_matched (game, SignalMatchType.DATA, 0, 0, null, null, this);
+        }
+
+        game = new Game (20, 14, settings.get_int ("starting-level"), settings.get_int ("line-fill-height"), settings.get_int ("line-fill-probability"), settings.get_boolean ("pick-difficult-blocks"));
+        game.shape_landed.connect (shape_landed_cb);
+        game.complete.connect (complete_cb);
+        preview.game = game;
+        view.game = game;
+
+        game.start ();
+
+        update_score ();
+        pause_action.sensitive = true;
+    }
+
+    private void shape_landed_cb (int[] lines, List<Block> line_blocks)
+    {
+        update_score ();
+    }
+
+    private void complete_cb ()
+    {
+        pause_action.sensitive = false;
+        if (game.score > 0)
+        {
+            var pos = high_scores.add_plain_score (game.score);
+            var dialog = new GnomeGamesSupport.ScoresDialog (main_window, high_scores, _("Quadrapassel Scores"));
+            var title = _("Puzzle solved!");
+            var message = _("You didn't make the top ten, better luck next time.");
+            if (pos == 1)
+                message = _("Your score is the best!");
+            else if (pos > 1)
+                message = _("Your score has made the top ten.");
+            dialog.set_message ("<b>%s</b>\n\n%s".printf (title, message));
+            dialog.set_buttons (GnomeGamesSupport.ScoresButtons.QUIT_BUTTON | GnomeGamesSupport.ScoresButtons.NEW_GAME_BUTTON);
+            if (pos > 0)
+                dialog.set_hilight (pos);
+
+            switch (dialog.run ())
+            {
+            case Gtk.ResponseType.REJECT:
+                Gtk.main_quit ();
+                break;
+            default:
+                new_game ();
+                break;
+            }
+            dialog.destroy ();
+        }
+    }
+
+    private void update_score ()
+    {
+        var score = 0;
+        var level = 0;
+        var n_lines_destroyed = 0;
+
+        if (game != null)
+        {
+            score = game.score;
+            level = game.level;
+            n_lines_destroyed = game.n_lines_destroyed;
+        }
+
+        score_label.set_text ("%d".printf (score));
+        level_label.set_text ("%d".printf (level));
+        n_destroyed_label.set_text ("%d".printf (n_lines_destroyed));
+    }
+
+    private void help_cb (Gtk.Action action)
+    {
+        try
+        {
+            Gtk.show_uri (main_window.get_screen (), "ghelp:quadrapassel", Gtk.get_current_event_time ());
+        }
+        catch (Error e)
+        {
+            warning ("Failed to show help: %s", e.message);
+        }
+    }
+
+    private void about_cb (Gtk.Action action)
+    {
+        string[] authors = { "Gnome Games Team", null };
+        string[] documenters = { "Angela Boyle", null };
+
+        Gtk.show_about_dialog (main_window,
+                               "program-name", _("Quadrapassel"),
+                               "version", VERSION,
+                               "comments", _("A classic game of fitting falling blocks together.\n\nQuadrapassel is a part of GNOME Games."),
+                               "copyright", "Copyright \xc2\xa9 1999 J. Marcin Gorycki, 2000-2009 Others",
+                               "license", GnomeGamesSupport.get_license (_("Quadrapassel")),
+                               "website-label", _("GNOME Games web site"),
+                               "authors", authors,
+                               "documenters", documenters,
+                               "translator-credits", _("translator-credits"),
+                               "logo-icon-name", "quadrapassel",
+                               "website", "http://wwmain_window.gnome.org/projects/gnome-games/";,
+                               "wrap-license", true,
+                               null);
+    }
+
+    private void scores_cb (Gtk.Action action)
+    {
+        var dialog = new GnomeGamesSupport.ScoresDialog (main_window, high_scores, _("Quadrapassel Scores"));
+        dialog.run ();
+        dialog.destroy ();
+    }
+
+    public static int main (string[] args)
+    {
+        Intl.setlocale (LocaleCategory.ALL, "");
+        Intl.bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
+        Intl.bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+        Intl.textdomain (GETTEXT_PACKAGE);
+
+        GnomeGamesSupport.scores_startup ();
+    
+        var context = new OptionContext ("");
+
+        context.add_group (Gtk.get_option_group (true));
+        context.add_group (Clutter.get_option_group_without_init ());
+
+        try
+        {
+            context.parse (ref args);
+        }
+        catch (Error e)
+        {
+            stderr.printf ("%s\n", e.message);
+            return Posix.EXIT_FAILURE;
+        }
+
+        Environment.set_application_name (_("Quadrapassel"));
+
+        Gtk.Window.set_default_icon_name ("quadrapassel");
+
+        try
+        {
+            GtkClutter.init_with_args (ref args, "", new OptionEntry[0], null);
+        }
+        catch (Error e)
+        {
+            var dialog = new Gtk.MessageDialog (null, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.NONE, "Unable to initialize Clutter:\n%s", e.message);
+            dialog.set_title (Environment.get_application_name ());
+            dialog.run ();
+            dialog.destroy ();
+            return Posix.EXIT_FAILURE;
+        }
+
+        var app = new Quadrapassel ();
+        app.show ();
+
+        Gtk.main ();
+
+        return Posix.EXIT_SUCCESS;
+    }
+}
diff --git a/sounds/Makefile.am b/sounds/Makefile.am
index ca51827..39c5515 100644
--- a/sounds/Makefile.am
+++ b/sounds/Makefile.am
@@ -9,20 +9,15 @@ sound_DATA = \
 	die.ogg \
 	flip-piece.ogg \
 	gameover.ogg \
-	gnometris.ogg \
 	gobble.ogg \
 	land.ogg \
 	laughter.ogg \
 	life.ogg \
-	lines1.ogg \
-	lines2.ogg \
-	lines3.ogg \
 	pop.ogg \
 	reverse.ogg \
 	slide.ogg \
 	splat.ogg \
 	teleport.ogg \
-	turn.ogg \
 	victory.ogg \
 	yahoo.ogg
 



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