Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoe Lee <joe@jotaro.com>2008-03-11 05:18:54 (GMT)
committer Joe Lee <joe@jotaro.com>2008-03-11 05:18:54 (GMT)
commit0044b6872ca1cd38e96c81d7f3ce21c5689b7f0c (patch)
treef95248f4cb71f17a128bb7903b07342fd4265def
Initial import
-rwxr-xr-xMANIFEST17
-rwxr-xr-xNEWS36
-rwxr-xr-xactivity/activity-implode.svg41
-rwxr-xr-xactivity/activity.info7
-rwxr-xr-xboard.py300
-rwxr-xr-xboardgen.py310
-rwxr-xr-xboardgentest.py356
-rwxr-xr-xcolor.py48
-rwxr-xr-xgridwidget.py699
-rwxr-xr-xicons/easy-level.svg72
-rwxr-xr-xicons/edit-redo.svg16
-rwxr-xr-xicons/edit-undo.svg15
-rwxr-xr-xicons/hard-level.svg128
-rwxr-xr-xicons/medium-level.svg96
-rwxr-xr-xicons/new-game.svg14
-rwxr-xr-xicons/replay-game.svg15
-rwxr-xr-ximplodeactivity.py126
-rwxr-xr-ximplodegame.py207
-rwxr-xr-xsetup.py5
-rwxr-xr-xsugarless.py102
20 files changed, 2610 insertions, 0 deletions
diff --git a/MANIFEST b/MANIFEST
new file mode 100755
index 0000000..156520b
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,17 @@
+activity/activity-implode.svg
+activity/activity.info
+board.py
+boardgen.py
+color.py
+gridwidget.py
+icons/easy-level.svg
+icons/edit-redo.svg
+icons/edit-undo.svg
+icons/hard-level.svg
+icons/medium-level.svg
+icons/new-game.svg
+icons/replay-game.svg
+implodeactivity.py
+implodegame.py
+MANIFEST
+NEWS
diff --git a/NEWS b/NEWS
new file mode 100755
index 0000000..6d6b790
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,36 @@
+
+2007/08/09 - Version 1 released.
+
+2007/11/19 - Version 2 released.
+
+ - Icon modified to display in Mediawiki renderer correctly (works fine
+ everywhere else).
+ - Added a reward graphic when the level is successfully cleared.
+ - Rewrote board data structure to one that should be more effective.
+ - Improved level generator. The old generator generated some unsolvable
+ levels, and the rest were solvable in a certain way that makes the game
+ easier once you know about it. The new one should be more robust.
+ - Implemented "new game" and "repeat game" functions.
+ - Completed sliding animation.
+ - Implemented undo/redo.
+ - Added "sugarless" file for developing in a non-Sugar environment.
+ - Ported sugarless code back to activity.
+ - Added new, retry, undo, redo, easy, medium, and hard icons.
+
+TODO:
+- Add a way to select difficulty.
+- Gamepad/keyboard controls need to be added.
+- Check for internationalizable text.
+- Maybe include a way to regenerate earlier games or games from another
+ laptop?
+- The game could detect a loss and display a "Try again" graphic?
+- The activity needs to save/restore the current game on exit/restart (maybe
+ using the Journal?).
+- Rectangle invalidation could be improved.
+- A tutorial mode or sequence of introductory games might help.
+- The code documentation and organization could be improved. Some
+ calculations are repeated.
+- Audio cues might be nice.
+- Tracking win/loss statistics might be nice.
+
+
diff --git a/activity/activity-implode.svg b/activity/activity-implode.svg
new file mode 100755
index 0000000..39ef91c
--- /dev/null
+++ b/activity/activity-implode.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY fill_color "#FFFFFF">
+ <!ENTITY stroke_color "#000000">
+]>
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ height="55"
+ version="1.0"
+ width="55"
+ id="Icon">
+ <path
+ d="M 10.805605,38.211574 L 16.010383,38.211574 C 16.896859,38.211574 17.610522,38.950024 17.610522,39.867292 L 17.610522,45.089349 C 17.610522,46.006617 16.896859,46.745068 16.010383,46.745068 L 10.805605,46.745068 C 9.919127,46.745068 9.2054655,46.006617 9.2054655,45.089349 L 9.2054655,39.867292 C 9.2054655,38.950024 9.919127,38.211574 10.805605,38.211574 z "
+ style="fill:&stroke_color;;fill-opacity:1;stroke:&stroke_color;;stroke-width:1.15481782;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ style="fill:&stroke_color;;fill-opacity:1;stroke:&stroke_color;;stroke-width:1.15481782;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 31.405605,38.211574 L 36.610383,38.211574 C 37.496859,38.211574 38.210522,38.950024 38.210522,39.867292 L 38.210522,45.089349 C 38.210522,46.006617 37.496859,46.745068 36.610383,46.745068 L 31.405605,46.745068 C 30.519127,46.745068 29.805466,46.006617 29.805466,45.089349 L 29.805466,39.867292 C 29.805466,38.950024 30.519127,38.211574 31.405605,38.211574 z " />
+ <path
+ style="fill:&fill_color;;fill-opacity:1;stroke:&stroke_color;;stroke-width:1.15481782;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 21.105605,38.211574 L 26.310383,38.211574 C 27.196859,38.211574 27.910522,38.950024 27.910522,39.867292 L 27.910522,45.089349 C 27.910522,46.006617 27.196859,46.745068 26.310383,46.745068 L 21.105605,46.745068 C 20.219127,46.745068 19.505466,46.006617 19.505466,45.089349 L 19.505466,39.867292 C 19.505466,38.950024 20.219127,38.211574 21.105605,38.211574 z " />
+ <path
+ style="fill:&stroke_color;;fill-opacity:1;stroke:&stroke_color;;stroke-width:1.15481782;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 41.705605,38.211574 L 46.910383,38.211574 C 47.796859,38.211574 48.510522,38.950024 48.510522,39.867292 L 48.510522,45.089349 C 48.510522,46.006617 47.796859,46.745068 46.910383,46.745068 L 41.705605,46.745068 C 40.819127,46.745068 40.105466,46.006617 40.105466,45.089349 L 40.105466,39.867292 C 40.105466,38.950024 40.819127,38.211574 41.705605,38.211574 z " />
+ <path
+ d="M 21.105605,27.911574 L 26.310383,27.911574 C 27.196859,27.911574 27.910522,28.650024 27.910522,29.567292 L 27.910522,34.789349 C 27.910522,35.706617 27.196859,36.445068 26.310383,36.445068 L 21.105605,36.445068 C 20.219127,36.445068 19.505466,35.706617 19.505466,34.789349 L 19.505466,29.567292 C 19.505466,28.650024 20.219127,27.911574 21.105605,27.911574 z "
+ style="fill:&fill_color;;fill-opacity:1;stroke:&stroke_color;;stroke-width:1.15481782;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ d="M 21.105605,17.611574 L 26.310383,17.611574 C 27.196859,17.611574 27.910522,18.350024 27.910522,19.267292 L 27.910522,24.489349 C 27.910522,25.406617 27.196859,26.145068 26.310383,26.145068 L 21.105605,26.145068 C 20.219127,26.145068 19.505466,25.406617 19.505466,24.489349 L 19.505466,19.267292 C 19.505466,18.350024 20.219127,17.611574 21.105605,17.611574 z "
+ style="fill:&fill_color;;fill-opacity:1;stroke:&stroke_color;;stroke-width:1.15481782;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ d="M 31.405605,27.911574 L 36.610383,27.911574 C 37.496859,27.911574 38.210522,28.650024 38.210522,29.567292 L 38.210522,34.789349 C 38.210522,35.706617 37.496859,36.445068 36.610383,36.445068 L 31.405605,36.445068 C 30.519127,36.445068 29.805466,35.706617 29.805466,34.789349 L 29.805466,29.567292 C 29.805466,28.650024 30.519127,27.911574 31.405605,27.911574 z "
+ style="fill:&fill_color;;fill-opacity:1;stroke:&stroke_color;;stroke-width:1.15481782;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ style="fill:&stroke_color;;fill-opacity:1;stroke:&stroke_color;;stroke-width:1.15481782;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 10.805605,27.911574 L 16.010383,27.911574 C 16.896859,27.911574 17.610522,28.650024 17.610522,29.567292 L 17.610522,34.789349 C 17.610522,35.706617 16.896859,36.445068 16.010383,36.445068 L 10.805605,36.445068 C 9.919127,36.445068 9.2054655,35.706617 9.2054655,34.789349 L 9.2054655,29.567292 C 9.2054655,28.650024 9.919127,27.911574 10.805605,27.911574 z " />
+ <path
+ style="fill:&stroke_color;;fill-opacity:1;stroke:&stroke_color;;stroke-width:1.15481782;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 31.405605,17.611574 L 36.610383,17.611574 C 37.496859,17.611574 38.210522,18.350024 38.210522,19.267292 L 38.210522,24.489349 C 38.210522,25.406617 37.496859,26.145068 36.610383,26.145068 L 31.405605,26.145068 C 30.519127,26.145068 29.805466,25.406617 29.805466,24.489349 L 29.805466,19.267292 C 29.805466,18.350024 30.519127,17.611574 31.405605,17.611574 z " />
+</svg>
diff --git a/activity/activity.info b/activity/activity.info
new file mode 100755
index 0000000..06435f8
--- /dev/null
+++ b/activity/activity.info
@@ -0,0 +1,7 @@
+[Activity]
+name = Implode
+activity_version = 2
+service_name = com.jotaro.ImplodeActivity
+icon = activity-implode
+class = implodeactivity.ImplodeActivity
+show_launcher = yes
diff --git a/board.py b/board.py
new file mode 100755
index 0000000..8c5aea3
--- /dev/null
+++ b/board.py
@@ -0,0 +1,300 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2007, Joseph C. Lee
+#
+# This program 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 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import random
+
+class Board(object):
+ """Object that defines a board containing pieces."""
+ # Board is represented as a dict from x coordinates to lists representing
+ # columns of pieces. The beginning of the lists are the value of the piece
+ # in the column at y=0, and subsequent values are for increasing values of
+ # y. Missing values are represented either with None in the column list or
+ # a missing column in the dict.
+ def __init__(self):
+ self._data = {}
+
+ def clone(self):
+ """Return a copy of the board."""
+ b = Board()
+ for (col_index, col) in self._data.items():
+ b._data[col_index] = col[:]
+ return b
+
+ def get_value(self, x, y):
+ """Return the value at coordinate (x,y), or None if no value is
+ present."""
+ col = self._data.get(x, None)
+ if col is None:
+ return None
+ if 0 <= y < len(col):
+ return col[y]
+ else:
+ return None
+
+ def set_value(self, x, y, value):
+ """Set the value at coordinate (x,y) to the given value."""
+ assert y >= 0
+
+ col = self._data.get(x, None)
+ if col is None:
+ if value is not None:
+ self._data[x] = [None] * y + [value]
+ elif y < len(col):
+ col[y] = value
+ if value is None:
+ self._trim_column(x)
+ elif value is None:
+ pass
+ else:
+ self._data[x] = col + [None] * (y - len(col)) + [value]
+
+ def get_column_height(self, x):
+ """Return the height of column x."""
+ col = self._data.get(x, None)
+ if col is None:
+ return 0
+ else:
+ return len(col)
+
+ @property
+ def width(self):
+ return (self.max_x - self.min_x)
+
+ @property
+ def height(self):
+ return (self.max_y - self.min_y)
+
+ @property
+ def min_x(self):
+ if len(self._data) == 0:
+ return 0
+ else:
+ return min(0, min(self._data.keys()))
+
+ @property
+ def max_x(self):
+ if len(self._data) == 0:
+ return 0
+ else:
+ return max(0, max(self._data.keys())) + 1
+
+ @property
+ def min_y(self):
+ return 0
+
+ @property
+ def max_y(self):
+ if len(self._data) == 0:
+ return 0
+ else:
+ return max(0, max(len(col) for col in self._data.values()))
+
+ def is_empty(self):
+ return (len(self._data) == 0)
+
+ def get_value_map(self):
+ """Returns a map from coordinate tuples to values for all cells on the
+ board."""
+ value_map = {}
+ for (i, col) in self._data.items():
+ for (j, value) in enumerate(col):
+ if value is not None:
+ value_map[(i, j)] = value
+ return value_map
+
+ def _trim_column(self, x):
+ # Removes any None values at the top of the given column, removing the
+ # column array entirely if it is empty.
+ col = self._data[x]
+ if col[-1] is not None:
+ return
+ for i in range(len(col) - 1, -1, -1):
+ if col[i] is not None:
+ self._data[x] = col[:i+1]
+ return
+ del self._data[x]
+
+ def get_all_contiguous(self):
+ """Returns a collection of all contiguous shapes with size >= 3,
+ where each contiguous shape is represented as a set of coordinate
+ tuples."""
+ examined = set()
+ all_contiguous = []
+ for (i, col) in self._data.items():
+ for (j, value) in enumerate(col):
+ coord = (i, j)
+ if coord not in examined:
+ examined.add(coord)
+ contiguous = self.get_contiguous(*coord)
+ examined.update(contiguous)
+ if len(contiguous) >= 3:
+ all_contiguous.append(contiguous)
+ return all_contiguous
+
+ def get_contiguous(self, x, y):
+ """Given a board coordinate, returns a set of all the coordinate
+ tuples that are contiguous and have the same value."""
+
+ value = self.get_value(x, y)
+ if value is None:
+ return set()
+
+ # Add the start location to the candidate and examined sets.
+ candidates = set()
+ candidates.add((x, y))
+ examined = set()
+ examined.add((x, y))
+
+ # Build the contiguous set.
+ contiguous = set()
+ while len(candidates) > 0:
+ coord = candidates.pop()
+ if self.get_value(coord[0], coord[1]) == value:
+ # If the candidate has the value we're looking for, add it
+ # to the contiguous set and add its unexamined neighbors to
+ # the candidate and examined sets.
+ contiguous.add(coord)
+ for (x_offset, y_offset) in ((1, 0), (-1, 0), (0, 1), (0, -1)):
+ coord2 = (coord[0] + x_offset, coord[1] + y_offset)
+ if coord2 not in examined:
+ examined.add(coord2)
+ candidates.add(coord2)
+ return contiguous
+
+ def remove_empty_columns(self):
+ """Removes columns that are empty."""
+ new_data = {}
+ for i in sorted(self._data.keys()):
+ new_data[len(new_data)] = self._data[i]
+ self._data = new_data
+
+ def get_slide_map(self):
+ """Returns a map showing where sliding pieces will go when empty
+ columns are removed, as a dictionary mapping old x coordinates
+ to new x coordinates. Does not include entries where the old
+ x coordinates are the same as the new ones."""
+ slide_map = {}
+ for (i, x) in enumerate(sorted(self._data.keys())):
+ if i != x:
+ slide_map[x] = i
+ return slide_map
+
+ def clear_pieces(self, pieces):
+ """Given a set of coordinate tuples, removes their contents from the
+ board."""
+ for (x, y) in pieces:
+ self.set_value(x, y, None)
+
+ def insert_columns(self, col_index, num_columns):
+ """Inserts empty columns at the given index, pushing higher-numbered
+ columns higher."""
+ assert num_columns >= 0
+ new_data = {}
+ for (i, col) in self._data.items():
+ if i < col_index:
+ new_data[i] = col
+ else:
+ new_data[i + num_columns] = col
+ self._data = new_data
+
+ def delete_columns(self, col_index, num_columns):
+ """Removes columns from the given location of the board, lowering the
+ higher-numbered columns to fill the space."""
+ assert 0 <= num_columns
+ new_data = {}
+ for (i, col) in self._data.items():
+ if i < col_index:
+ new_data[i] = col
+ elif i >= col_index + num_columns:
+ new_data[i - num_columns] = col
+ self._data = new_data
+
+ def get_empty_columns(self):
+ """Returns a list of empty (all zero) columns."""
+ empty_cols = []
+ for i in range(min(0, self.min_x), self.max_x):
+ if i not in self._data.items():
+ empty_cols.append(i)
+ return empty_cols
+
+ def drop_pieces(self):
+ for (i, col) in self._data.items():
+ self._data[i] = [x for x in col if x is not None]
+
+ def get_drop_map(self):
+ """Returns a map showing where dropped pieces will go (compacting
+ out None values vertically), as a dictionary mapping old
+ coordinate tuples to new coordinate tuples."""
+ drop_map = {}
+ for (i, col) in self._data.items():
+ offset = 0
+ for (j, value) in enumerate(col):
+ if value is not None:
+ drop_map[(i, j)] = (i, offset)
+ offset += 1
+ return drop_map
+
+ def __eq__(self, other):
+ return (self._data == other._data)
+
+ def __neq__(self, other):
+ return not self.__eq__(other)
+
+ def __repr__(self):
+ width = self.width
+ height = self.height
+ lines = []
+ for i in reversed(range(height)):
+ line = []
+ for j in range(width):
+ value = self.get_value(j, i)
+ if value is None:
+ line.append('.')
+ elif value == -1:
+ line.append('*')
+ else:
+ line.append(str(value))
+ lines.append(''.join(line))
+ return '\n'.join(lines)
+
+def make_test_board(width, height):
+ b = Board()
+ r = random.Random()
+ r.seed(0)
+ unchosen = []
+ for x in range(width):
+ for y in range(height):
+ unchosen.append((x, y))
+ for i in range(width * height * 4 / 6):
+ coord = r.choice(unchosen)
+ unchosen.remove(coord)
+ b.set_value(coord[0], coord[1], r.randint(0, 2))
+ for i in range(10 + 1):
+ b.set_value(i, 0, i)
+ return b
+
+def dump_board(b):
+ print repr(b)
+
+def main():
+ b = make_test_board(30, 20)
+ dump_board(b)
+ print b.get_all_contiguous()
+
+if __name__ == '__main__':
+ main()
diff --git a/boardgen.py b/boardgen.py
new file mode 100755
index 0000000..1d3aaf6
--- /dev/null
+++ b/boardgen.py
@@ -0,0 +1,310 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2007, Joseph C. Lee
+#
+# This program 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 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import math
+import random
+
+import board
+
+def generate_board(seed=0,
+ fragmentation=1,
+ fill=0.5,
+ max_colors=5,
+ max_size=(30,20)):
+ """Generates a new board of the given properties using the given random
+ seed as a starting point."""
+ r = random.Random(seed)
+ piece_sizes = _get_piece_sizes(r, fragmentation, fill, max_size)
+ b = board.Board()
+ for piece_size in piece_sizes:
+ b = _try_add_piece(b, r, piece_size, max_colors, max_size)
+ return b
+
+def _try_add_piece(b, r, piece_size, max_colors, max_size):
+ # Tries to add a piece of the given size to the board. Returns the
+ # modified board on success or the original board on failure.
+ b2 = b.clone()
+ change = _get_starting_change(b2, r, max_colors, max_size)
+ if change is None:
+ # If there are no valid starting points, return the original board.
+ return b
+ _make_change(b2, change)
+ total_added_cells = 1
+ while total_added_cells < piece_size:
+ added_cells = _try_add_cells(b2, r, max_colors, max_size)
+ if added_cells > 0:
+ total_added_cells += added_cells
+ else:
+ # If we have added a valid piece, return the modified board;
+ # otherwise return the original board.
+ if total_added_cells >= 3:
+ break
+ else:
+ #print "Aborted piece add."
+ return b
+ _color_piece_random(b2, r, max_colors)
+ return b2
+
+def _get_starting_change(b, r, max_colors, max_size):
+ # Gets a valid initial change that adds a one-cell colorable piece to the
+ # board, returning None if no such starting change exists.
+ changes = _enumerate_one_cell_changes(b, max_size)
+ while len(changes) > 0:
+ change = r.choice(changes)
+ changes.remove(change)
+ if _change_is_colorable(b, change, max_colors):
+ return change
+ return None
+
+def _enumerate_one_cell_changes(b, max_size):
+ # Returns a list of all possible one-cell changes.
+ (max_width, max_height) = max_size
+ changes = []
+ width = b.width
+ if width < max_width and max_height >= 1:
+ for i in range(width + 1):
+ changes.append(_InsertColumnChange(i, 1))
+ for i in range(width):
+ col_height = b.get_column_height(i)
+ if col_height < max_height:
+ for j in range(col_height + 1):
+ changes.append(_InsertCellChange(i, j))
+ return changes
+
+def _try_add_cells(b, r, max_colors, max_size):
+ # Tries to add a cell or cells to the new piece on the board in a way that
+ # ensures the resulting board is within the given board size and is
+ # colorable with the given colors. Returns the number of cells added
+ # (zero, if no cell could be added).
+ (cell_h_changes, cell_v_changes) = _get_cell_changes(b, max_size)
+ col_changes = _get_col_changes(b, max_size)
+ while (len(cell_h_changes) > 0
+ or len(cell_v_changes) > 0
+ or len(col_changes) > 0):
+ change = _remove_change(r, cell_h_changes, cell_v_changes, col_changes)
+ if _change_is_colorable(b, change, max_colors):
+ _make_change(b, change)
+ #print
+ #print change
+ #print b
+ if isinstance(change, _InsertCellChange):
+ return 1
+ else:
+ return change.height
+ return 0
+
+def _get_cell_changes(b, max_size):
+ # Returns a list of all possible standard cell insertions.
+ (max_width, max_height) = max_size
+ h_changes = []
+ v_changes = []
+ width = b.width
+ for i in range(width):
+ col_height = b.get_column_height(i)
+ if col_height < max_height:
+ for j in range(col_height + 1):
+ if b.get_value(i, j) != -1:
+ if (b.get_value(i + 1, j) == -1 or
+ b.get_value(i - 1, j) == -1):
+ h_changes.append(_InsertCellChange(i, j))
+ elif (b.get_value(i, j - 1) == -1 or
+ b.get_value(i, j + 1) == -1):
+ v_changes.append(_InsertCellChange(i, j))
+ return h_changes, v_changes
+
+def _get_col_changes(b, max_size):
+ # Returns a list of all possible column insertions.
+ (max_width, max_height) = max_size
+ width = b.width
+ if width == max_width or max_height < 1:
+ return []
+ highest_new_pieces = []
+ for i in range(width):
+ col_height = b.get_column_height(i)
+ highest_new_piece = 0
+ for j in range(col_height):
+ value = b.get_value(i, j)
+ if value == -1:
+ highest_new_piece = j + 1
+ highest_new_pieces.append(highest_new_piece)
+ changes = []
+ for (i, (height1, height2)) in enumerate(zip(highest_new_pieces + [0],
+ [0] + highest_new_pieces)):
+ height = max(height1, height2)
+ if height > 0:
+ changes.append(_InsertColumnChange(i, height))
+ return changes
+
+def _remove_change(r, cell_h_changes, cell_v_changes, col_changes):
+ # Removes a change from cell changes or col changes (less likely) and
+ # returns it.
+ h_weight = len(cell_h_changes) * 10
+ v_weight = len(cell_v_changes) * 5
+ col_weight = len(col_changes) * 1
+ value = r.randint(0, h_weight + v_weight + col_weight - 1)
+ if value < h_weight:
+ return _pick(r, cell_h_changes)
+ elif value < h_weight + v_weight:
+ return _pick(r, cell_v_changes)
+ else:
+ return _pick(r, col_changes)
+
+def _pick(r, items):
+ index = r.randint(0, len(items) - 1)
+ return items.pop(index)
+
+def _change_is_colorable(b, change, max_colors):
+ # Returns True if the board is still colorable after the given change is
+ # made, False otherwise.
+ b2 = b.clone()
+ _make_change(b2, change)
+ colors = _get_new_piece_colors(b2, max_colors)
+ return len(colors) > 0
+
+def _make_change(b, change):
+ # Makes the given change to the board (side-affects board parameter).
+ if isinstance(change, _InsertColumnChange):
+ b.insert_columns(change.col, 1)
+ for i in range(change.height):
+ b.set_value(change.col, i, -1)
+ elif isinstance(change, _InsertCellChange):
+ new_indexes = []
+ data = []
+ col_height = b.get_column_height(change.col)
+ assert change.height <= col_height
+ for i in range(col_height):
+ value = b.get_value(change.col, i)
+ if i == change.height:
+ data.append(-1)
+ if value == -1:
+ new_indexes.append(i)
+ else:
+ data.append(value)
+ if change.height == col_height:
+ data.append(-1)
+ for index in new_indexes:
+ data.insert(index, -1)
+ for (i, value) in enumerate(data):
+ b.set_value(change.col, i, value)
+ else:
+ assert False
+
+def _color_piece_random(b, r, max_colors):
+ # Colors in the new piece on the board with a random color using the given
+ # random number generator and number of colors.
+ colors = _get_new_piece_colors(b, max_colors)
+ color = r.choice(list(colors))
+ _color_piece(b, color)
+
+def _color_piece(b, color):
+ # Colors in the new piece on the board with the given color.
+ coords = _get_new_piece_coords(b)
+ for (i, j) in coords:
+ b.set_value(i, j, color)
+
+def _get_new_piece_colors(b, max_colors):
+ # Returns the set of possible colors for the new piece.
+ colors = set(range(1, max_colors + 1))
+ coords = _get_new_piece_coords(b)
+ for (i, j) in coords:
+ for (x_ofs, y_ofs) in ((-1, 0), (1, 0), (0, -1), (0, 1)):
+ colors.discard(b.get_value(i + x_ofs, j + y_ofs))
+ return colors
+
+def _get_new_piece_coords(b):
+ # Returns a list of new piece coordinates.
+ coords = []
+ for i in range(b.width):
+ col_height = b.get_column_height(i)
+ for j in range(col_height):
+ if b.get_value(i, j) == -1:
+ coords.append((i, j))
+ return coords
+
+def _get_piece_sizes(r, fragmentation, fill, max_size):
+ # Returns a list containing the new piece sizes for the board using the
+ # given random number generator, fragmentation, fill, and board size.
+ max_area = max_size[0] * max_size[1] * fill
+ total_area = 0
+ piece_sizes = []
+ while total_area < max_area:
+ piece_size = _get_piece_size(r, fragmentation, max_area)
+ total_area += piece_size
+ piece_sizes.append(piece_size)
+ #print piece_sizes
+ return piece_sizes
+
+def _get_piece_size(r, fragmentation, max_area):
+ # Returns a random piece size using the given random number generator,
+ # fragmentation, and board size.
+ upper_bound = math.ceil(math.sqrt(max_area))
+ value = r.random()
+ exp = fragmentation
+ piece_size = int(max(3, math.pow(value, exp) * upper_bound))
+ return piece_size
+
+class _InsertColumnChange(object):
+ # Represents the action of inserting a column into the board at column
+ # "col" containing "height" cells.
+ def __init__(self, col, height):
+ self.col = col
+ self.height = height
+
+ def __eq__(self, other):
+ return (isinstance(other, _InsertColumnChange)
+ and self.col == other.col
+ and self.height == other.height)
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __repr__(self):
+ return "_InsertColumnChange(%d, %d)" % (self.col, self.height)
+
+class _InsertCellChange(object):
+ # Represents the action of inserting a cell into the board in column
+ # "col" at height "height".
+ def __init__(self, col, height):
+ self.col = col
+ self.height = height
+
+ def __eq__(self, other):
+ return (isinstance(other, _InsertCellChange)
+ and self.col == other.col
+ and self.height == other.height)
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __repr__(self):
+ return "_InsertCellChange(%d, %d)" % (self.col, self.height)
+
+def main():
+ b = generate_board(seed=1,
+ fragmentation=1,
+ max_colors=5,
+ max_size=(20,10))
+ print repr(b)
+
+if __name__ == '__main__':
+ #import cProfile
+ #cProfile.run('main()', 'genprof')
+ #import pstats
+ #p = pstats.Stats('genprof')
+ #p.strip_dirs().sort_stats(-1).print_stats()
+ main()
diff --git a/boardgentest.py b/boardgentest.py
new file mode 100755
index 0000000..a26e5f3
--- /dev/null
+++ b/boardgentest.py
@@ -0,0 +1,356 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2007, Joseph C. Lee
+#
+# This program 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 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import random
+import unittest
+
+import board
+import boardgen
+
+class TestEnumerateOneCellChanges(unittest.TestCase):
+
+ def test1(self):
+ b = board.Board()
+ expChanges = [boardgen._InsertColumnChange(0, 1)]
+ changes = boardgen._enumerate_one_cell_changes(b, (1, 1))
+ self._assertChanges(changes, expChanges)
+
+ def test2(self):
+ b = board.Board()
+ expChanges = []
+ changes = boardgen._enumerate_one_cell_changes(b, (0, 1))
+ self._assertChanges(changes, expChanges)
+
+ def test3(self):
+ b = board.Board()
+ expChanges = []
+ changes = boardgen._enumerate_one_cell_changes(b, (1, 0))
+ self._assertChanges(changes, expChanges)
+
+ def test4(self):
+ b = _make_board(""".1.
+ 211""")
+ cell = boardgen._InsertCellChange
+ col = boardgen._InsertColumnChange
+ expChanges = [col(0, 1),
+ col(1, 1),
+ col(2, 1),
+ col(3, 1),
+ cell(0, 0),
+ cell(0, 1),
+ cell(1, 0),
+ cell(1, 1),
+ cell(1, 2),
+ cell(2, 0),
+ cell(2, 1)]
+ changes = boardgen._enumerate_one_cell_changes(b, (10, 10))
+ self._assertChanges(changes, expChanges)
+
+ def _assertChanges(self, changes, expChanges):
+ self.assertEqual(len(changes), len(expChanges))
+ for change in changes:
+ self.assert_(change in expChanges)
+ for change in expChanges:
+ self.assert_(change in changes)
+
+class TestChangeIsColorable(unittest.TestCase):
+ pass # TODO
+
+class TestMakeChange(unittest.TestCase):
+ def testCol1(self):
+ before = ""
+ change = boardgen._InsertColumnChange(0, 1)
+ after = """*"""
+ self._assertMakeChange(before, change, after)
+
+ def testCol2(self):
+ before = """1"""
+ change = boardgen._InsertColumnChange(0, 1)
+ after = """*1"""
+ self._assertMakeChange(before, change, after)
+
+ def testCol3(self):
+ before = """1
+ 1"""
+ change = boardgen._InsertColumnChange(1, 1)
+ after = """1.
+ 1*"""
+ self._assertMakeChange(before, change, after)
+
+ def testCol4(self):
+ before = """1
+ 1"""
+ change = boardgen._InsertColumnChange(2, 3)
+ after = """..*
+ 1.*
+ 1.*"""
+ self._assertMakeChange(before, change, after)
+
+ def testCol5(self):
+ before = """.1.3
+ 2113"""
+ change = boardgen._InsertColumnChange(2, 2)
+ after = """.1*.3
+ 21*13"""
+ self._assertMakeChange(before, change, after)
+
+ def testCell1(self):
+ before = ""
+ change = boardgen._InsertCellChange(0, 0)
+ after = "*"
+ self._assertMakeChange(before, change, after)
+
+ def testCell2(self):
+ before = """1"""
+ change = boardgen._InsertCellChange(0, 0)
+ after = """1
+ *"""
+ self._assertMakeChange(before, change, after)
+
+ def testCell3(self):
+ before = """1"""
+ change = boardgen._InsertCellChange(0, 1)
+ after = """*
+ 1"""
+ self._assertMakeChange(before, change, after)
+
+ def testCell4(self):
+ before = """1
+ 2
+ 3
+ *
+ 4
+ 5"""
+ change = boardgen._InsertCellChange(0, 1)
+ after = """1
+ 2
+ 3
+ 4
+ *
+ *
+ 5"""
+ self._assertMakeChange(before, change, after)
+
+ def testCell5(self):
+ before = """1
+ 2
+ 3
+ *
+ 4
+ 5"""
+ change = boardgen._InsertCellChange(0, 2)
+ after = """1
+ 2
+ 3
+ *
+ *
+ 4
+ 5"""
+ self._assertMakeChange(before, change, after)
+
+ def testCell6(self):
+ before = """1
+ 2
+ 3
+ *
+ 4
+ 5"""
+ change = boardgen._InsertCellChange(0, 3)
+ after = """1
+ 2
+ 3
+ *
+ *
+ 4
+ 5"""
+ self._assertMakeChange(before, change, after)
+
+ def testCell7(self):
+ before = """1
+ 2
+ 3
+ *
+ 4
+ 5"""
+ change = boardgen._InsertCellChange(0, 4)
+ after = """1
+ 2
+ *
+ 3
+ *
+ 4
+ 5"""
+ self._assertMakeChange(before, change, after)
+
+ def testCell8(self):
+ before = """1
+ *
+ *
+ *
+ *
+ 5"""
+ change = boardgen._InsertCellChange(0, 0)
+ after = """1
+ 5
+ *
+ *
+ *
+ *
+ *"""
+ self._assertMakeChange(before, change, after)
+
+ def _assertMakeChange(self, before, change, after):
+ b = _make_board(before)
+ expBoard = _make_board(after)
+ boardgen._make_change(b, change)
+ self.assertEqual(b, expBoard)
+
+class TestChangeIsColorable(unittest.TestCase):
+ def test1(self):
+ b = _make_board("""""")
+ change = boardgen._InsertCellChange(0, 0)
+ self.assert_(boardgen._change_is_colorable(b, change, 1))
+
+ def test2(self):
+ b = _make_board("""1""")
+ change = boardgen._InsertCellChange(0, 0)
+ self.failIf(boardgen._change_is_colorable(b, change, 1))
+
+ def test3(self):
+ b = _make_board("""1""")
+ change = boardgen._InsertCellChange(0, 0)
+ self.assert_(boardgen._change_is_colorable(b, change, 2))
+
+ def test4(self):
+ b = _make_board("""1.2
+ 1*3""")
+ change = boardgen._InsertCellChange(1, 0)
+ self.failIf(boardgen._change_is_colorable(b, change, 2))
+
+ def test5(self):
+ b = _make_board("""1.2
+ 1*3""")
+ change = boardgen._InsertCellChange(1, 0)
+ self.failIf(boardgen._change_is_colorable(b, change, 3))
+
+class TestGetCellChanges(unittest.TestCase):
+
+ def test1(self):
+ s = """"""
+ expChanges = []
+ self._assertCellChanges(s, expChanges, (0, 0))
+
+ def test2(self):
+ s = """1"""
+ expChanges = []
+ self._assertCellChanges(s, expChanges, (1, 2))
+
+ def test3(self):
+ s = """*"""
+ expChanges = [boardgen._InsertCellChange(0, 1)]
+ self._assertCellChanges(s, expChanges, (1, 2))
+
+ def test4(self):
+ s = """.**
+ 12*"""
+ expChanges = [boardgen._InsertCellChange(0, 1),
+ boardgen._InsertCellChange(1, 0),
+ boardgen._InsertCellChange(1, 2),
+ boardgen._InsertCellChange(2, 2)]
+ self._assertCellChanges(s, expChanges, (3, 3))
+
+ def test5(self):
+ s = """.*.
+ .1.
+ 111"""
+ expChanges = [boardgen._InsertCellChange(1, 1),
+ boardgen._InsertCellChange(1, 3)]
+ self._assertCellChanges(s, expChanges, (3, 4))
+
+ def _assertCellChanges(self, s, expChanges, board_size):
+ b = _make_board(s)
+ (h_changes, v_changes) = boardgen._get_cell_changes(b, board_size)
+ changes = h_changes + v_changes
+ print changes
+ self.assertEqual(len(changes), len(expChanges))
+ for change in changes:
+ self.assert_(change in expChanges)
+ for change in expChanges:
+ self.assert_(change in changes)
+
+class TestGetColChanges(unittest.TestCase):
+
+ def test1(self):
+ s = """"""
+ expChanges = []
+ self._assertCellChanges(s, expChanges, (2, 2))
+
+ def test2(self):
+ s = """1.
+ 12"""
+ expChanges = []
+ self._assertCellChanges(s, expChanges, (3, 3))
+
+ def test3(self):
+ s = """*.
+ 12"""
+ expChanges = [boardgen._InsertColumnChange(0, 2),
+ boardgen._InsertColumnChange(1, 2)]
+ self._assertCellChanges(s, expChanges, (3, 3))
+
+ def test4(self):
+ s = """*.
+ **"""
+ expChanges = [boardgen._InsertColumnChange(0, 2),
+ boardgen._InsertColumnChange(1, 2),
+ boardgen._InsertColumnChange(2, 1)]
+ self._assertCellChanges(s, expChanges, (3, 3))
+
+ def _assertCellChanges(self, s, expChanges, board_size):
+ b = _make_board(s)
+ changes = boardgen._get_col_changes(b, board_size)
+ self.assertEqual(len(changes), len(expChanges))
+ for change in changes:
+ self.assert_(change in expChanges)
+ for change in expChanges:
+ self.assert_(change in changes)
+
+def _make_board(s):
+ b = board.Board()
+ # Constructs a board using the given string.
+ lines = [x.strip() for x in s.strip().splitlines()]
+
+ if len(lines) == 0:
+ return b
+
+ # Make sure all lines are the same length.
+ lens = [len(x) for x in lines]
+ assert len(set(lens)) == 1
+
+ val_map = {'.' : None, '*' : -1}
+ for i in range(1, 9 + 1):
+ val_map[str(i)] = i
+
+ for (i, line) in enumerate(reversed(lines)):
+ for (j, ch) in enumerate(line):
+ b.set_value(j, i, val_map[ch])
+
+ return b
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/color.py b/color.py
new file mode 100755
index 0000000..8ea70b3
--- /dev/null
+++ b/color.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2007, Joseph C. Lee
+#
+# This program 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 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+# Derived from Sugar list of colors.
+colors = (
+ (0.368627, 0.000000, 0.549020), # 5E008C
+ (0.901961, 0.000000, 0.039216), # E6000A
+ (0.000000, 0.917647, 0.066667), # 00EA11
+ (1.000000, 0.980392, 0.000000), # FFFA00
+ (0.498039, 0.000000, 0.749020), # 7F00BF
+ (1.000000, 0.560784, 0.000000), # FF8F00
+ (0.000000, 0.372549, 0.894118), # 005FE4
+ (0.603922, 0.321569, 0.000000), # 9A5200
+ (0.000000, 0.345098, 0.549020), # 00588C
+ (0.737255, 0.807843, 1.000000), # BCCEFF
+ (1.000000, 0.678431, 0.807843), # FFADCE
+ (0.000000, 0.627451, 1.000000), # 00A0FF
+ (0.788235, 0.494118, 0.000000), # C97E00
+ (0.000000, 0.698039, 0.050980), # 00B20D
+ (0.545098, 1.000000, 0.478431), # 8BFF7A
+ (0.501961, 0.458824, 0.000000), # 807500
+ (0.674510, 0.196078, 1.000000), # AC32FF
+ (0.654902, 0.000000, 1.000000), # A700FF
+ (0.972549, 0.909804, 0.000000), # F8E800
+ (0.000000, 0.501961, 0.035294), # 008009
+ (0.600000, 0.000000, 0.901961), # 9900E6
+ (0.745098, 0.619608, 0.000000), # BE9E00
+ (1.000000, 0.756863, 0.411765), # FFC169
+ (0.698039, 0.000000, 0.031373), # B20008
+ (0.819608, 0.639216, 1.000000), # D1A3FF
+ (1.000000, 0.168627, 0.203922), # FF2B34
+ (0.737255, 0.803922, 1.000000), # BCCDFF
+)
diff --git a/gridwidget.py b/gridwidget.py
new file mode 100755
index 0000000..ea2d6e6
--- /dev/null
+++ b/gridwidget.py
@@ -0,0 +1,699 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2007, Joseph C. Lee
+#
+# This program 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 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+_logger = logging.getLogger('implode-activity.gridwidget')
+
+import cairo
+import gobject
+import gtk
+import math
+import random
+
+import color
+
+# Color of the background.
+_BG_COLOR = (0.5, 0.5, 0.7)
+
+# Color of the selection border.
+_SELECTED_COLOR = (1.0, 1.0, 1.0)
+
+# Ratio of the width/height (whichever is smaller) to leave as a margin
+# around the playing board.
+_BORDER = 0.05
+
+# Ratio of the cell width to leave as a space between blocks.
+_BLOCK_GAP = 0.1
+
+# Ratio of the cell width to overdraw the selection border.
+_SELECTED_MARGIN = 0.1
+
+# Ratio of the cell width to use for the radius of the selection cursor circle.
+_SELECTED_DOT_RADIUS = 0.1
+
+# Smiley face.
+_SMILEY = """
+ ..xxxxxx..
+ .x......x.
+ x........x
+ x..x..x..x
+ x........x
+ x.x....x.x
+ x..xxxx..x
+ .x......x.
+ ..xxxxxx..
+"""
+
+# Animation modes.
+ANIMATE_NONE = 0
+ANIMATE_SHRINK = 1
+ANIMATE_FALL = 2
+ANIMATE_SLIDE = 3
+ANIMATE_ZOOM = 4
+ANIMATE_WIN = 5
+
+#import traceback
+#def _log_errors(func):
+# # A function decorator to add error logging to selected functions.
+# # (For when GTK eats exceptions).
+# def wrapper(*args, **kwargs):
+# try:
+# return func(*args, **kwargs)
+# except:
+# _logger.debug(traceback.format_exc())
+# raise
+# return wrapper
+def _log_errors(func):
+ return func
+
+class GridWidget(gtk.DrawingArea):
+ """Gtk widget for rendering the game board."""
+
+ __gsignals__ = {
+ 'piece-selected': (gobject.SIGNAL_RUN_LAST, None, (int, int)),
+ 'button-press-event': 'override',
+ 'expose-event': 'override',
+ 'size-allocate': 'override',
+ 'motion-notify-event': 'override',
+ }
+
+ def __init__(self, *args, **kwargs):
+ super(GridWidget, self).__init__(*args, **kwargs)
+ self.set_events(gtk.gdk.BUTTON_PRESS_MASK
+ | gtk.gdk.POINTER_MOTION_MASK)
+ self._board = None
+ self._board_width = 0
+ self._board_height = 0
+ self._removal_block_set = set()
+ self._animation_percent = 0.0
+ self._animation_mode = ANIMATE_NONE
+ self._selected_cell = None
+ self._contiguous_map = {}
+
+ # Game animation variables.
+ self._animation_coords = []
+ self._animation_frames = {}
+ self._animation_lengths = {}
+
+ # Winning animation variables.
+ self._win_coords = []
+ self._win_starts = []
+ self._win_ends = []
+ self._win_length = 0
+ self._win_size = (0,0)
+ self._win_transform = None
+ self._win_draw_flag = False
+ self._win_color = 0
+
+ # Drawing offset and scale.
+ self._board_transform = None
+
+ def set_board(self, value):
+ self._board = value
+ self._recalc_board_dimensions()
+ self._recalc_contiguous_map()
+ self._init_board_layout(self.allocation.width,
+ self.allocation.height)
+ self._invalidate_board()
+
+ def set_removal_block_set(self, value):
+ self._removal_block_set = value
+ self._recalc_game_animation_frames()
+
+ def set_animation_mode(self, value):
+ self._animation_mode = value
+ if value == ANIMATE_WIN:
+ self._recalc_win_animation_frames()
+ self._invalidate_board()
+
+ def set_animation_percent(self, value):
+ self._animation_percent = value
+ self._recalc_animation_coords()
+
+ def set_win_draw_flag(self, value):
+ if self._win_draw_flag != value:
+ self._win_draw_flag = value
+ self._invalidate_board()
+
+ def get_animation_length(self):
+ if self._animation_mode == ANIMATE_NONE:
+ return 0.0
+ if self._animation_mode == ANIMATE_WIN:
+ return self._win_length
+ else:
+ return self._animation_lengths[self._animation_mode]
+
+ def _recalc_contiguous_map(self):
+ self._contiguous_map = {}
+ if self._board is None:
+ return
+ all_contiguous = self._board.get_all_contiguous()
+ for contiguous in all_contiguous:
+ for coord in contiguous:
+ self._contiguous_map[coord] = contiguous
+
+ @_log_errors
+ def do_button_press_event(self, event):
+ # Ignore mouse clicks while animating.
+ if self._animation_mode != ANIMATE_NONE:
+ return
+ self._set_mouse_selection(event.x, event.y)
+ if self._selected_cell is not None:
+ self.emit('piece-selected', *self._selected_cell)
+
+ @_log_errors
+ def do_motion_notify_event(self, event):
+ if event.is_hint:
+ (x, y, state) = event.window.get_pointer()
+ else:
+ x = event.x
+ y = event.y
+ state = event.state
+ self._set_mouse_selection(x, y)
+
+ def _set_mouse_selection(self, x, y):
+ if not self._board_is_valid():
+ self._selected_cell = None
+ return
+ old_selection = self._selected_cell
+ (x1, y1) = self._display_to_cell(x, y)
+ if (0 <= x1 < self._board_width and 0 <= y1 < self._board_height):
+ self._selected_cell = (x1, y1)
+ self._invalidate_selection(old_selection)
+ self._invalidate_selection(self._selected_cell)
+
+ def _invalidate_selection(self, selection_coord):
+ contiguous = self._contiguous_map.get(selection_coord, None)
+ if contiguous is not None and len(contiguous) >= 3:
+ self._invalidate_block_set(contiguous, _SELECTED_MARGIN)
+ elif selection_coord is not None:
+ self._invalidate_block_set(set((selection_coord,)), 0)
+
+ def _invalidate_block_set(self, block_set, margin):
+ if len(block_set) == 0:
+ return
+ x_coords = [q[0] for q in block_set]
+ y_coords = [q[1] for q in block_set]
+ min_x1 = min(x_coords) - margin
+ max_x1 = max(x_coords) + margin + 1
+ min_y1 = min(y_coords) - margin
+ max_y1 = max(y_coords) + margin + 1
+ pt1 = self._cell_to_display(min_x1, min_y1)
+ pt2 = self._cell_to_display(max_x1, max_y1)
+ min_x2 = math.floor(min(pt1[0], pt2[0])) - 1
+ max_x2 = math.ceil( max(pt1[0], pt2[0])) + 1
+ min_y2 = math.floor(min(pt1[1], pt2[1])) - 1
+ max_y2 = math.ceil( max(pt1[1], pt2[1])) + 1
+ if self.window:
+ rect = gtk.gdk.Rectangle(int(min_x2),
+ int(min_y2),
+ int(max_x2 - min_x2),
+ int(max_y2 - min_y2))
+ self.window.invalidate_rect(rect, True)
+
+ def _invalidate_board(self):
+ if self.window:
+ alloc = self.allocation
+ rect = gtk.gdk.Rectangle(0, 0, alloc.width, alloc.height)
+ self.window.invalidate_rect(rect, True)
+
+ def _display_to_cell(self, x, y):
+ # Converts from display coordinate to a cell coordinate.
+ return self._board_transform.inverse_transform(x, y)
+
+ def _cell_to_display(self, x, y):
+ # Converts from a cell coordinate to a display coordinate.
+ return self._board_transform.transform(x, y)
+
+ def _recalc_win_animation_frames(self):
+ r = random.Random()
+ r.seed()
+ (tiles, width, height) = self._get_win_tiles()
+ tiles = self._reorder_win_tiles(r, tiles, width, height)
+ self._win_starts = self._get_win_starts(tiles, width, height)
+ self._win_ends = self._get_win_ends(tiles)
+ self._win_length = self._get_win_length()
+ self._win_size = (width, height)
+ self._win_color = r.randint(1, 5)
+ self._recalc_win_transform(self.allocation.width,
+ self.allocation.height)
+
+ def _get_win_tiles(self):
+ # Returns a list of ending tile coordinates making up the smiley face,
+ # as well as the width and height in tiles.
+ data = [list(x.strip()) for x in _SMILEY.strip().splitlines()]
+ height = len(data)
+ widths = set([len(x) for x in data])
+ assert len(widths) == 1
+ width = widths.pop()
+ assert width > 0
+ assert height > 0
+ tiles = []
+ for i in range(height):
+ for j in range(width):
+ if data[i][j] == 'x':
+ # Invert y axis because we use the board tile engine to
+ # display, which uses cartesian coordinates instead of
+ # display coordinates.
+ tiles.append((j, height - i - 1))
+ return (tiles, width, height)
+
+ def _reorder_win_tiles(self, r, tiles, width, height):
+ # Re-sorts tiles by several randomly chosen criteria.
+ def radial(coord):
+ (x, y) = coord
+ x = float(x) / width - 0.5
+ y = float(y) / height - 0.5
+ return 2 * math.sqrt(x * x + y * y)
+ def x(coord):
+ return float(coord[0]) / width
+ def y(coord):
+ return float(coord[1]) / height
+ def angle(coord):
+ (x, y) = coord
+ x = float(x) / width - 0.5
+ y = float(y) / height - 0.5
+ angle = math.atan2(y, x)
+ return (angle / math.pi + 1) / 2
+ funcs = [radial, x, y, angle]
+ r.shuffle(funcs)
+ invs = [r.choice((-1, 1)), r.choice((-1, 1))]
+ pairs = []
+ w = r.random()
+ for coord in tiles:
+ score = funcs[0](coord) * invs[0] + funcs[1](coord) * invs[1] * w
+ pairs.append((score, coord))
+ pairs.sort()
+ # Re-interleave pairs, if desired.
+ if r.randint(0, 1):
+ index1 = int(len(pairs) / 2)
+ list1 = pairs[:index1]
+ list2 = pairs[index1:]
+ if r.randint(0, 1):
+ list2.reverse()
+ pairs = _interleave(list1, list2)
+ return [x[1] for x in pairs]
+
+ def _get_win_starts(self, tiles, width, height):
+ # Returns a list of starting coordinates for tiles.
+ starts = []
+ assert width > 0
+ assert height > 0
+ start_x = width / 2.0 - 0.5
+ start_y = height / 2.0 - 0.5
+ for (i, (x, y)) in enumerate(tiles):
+ starts.append((i, start_x, start_y, 0.0))
+ #starts.append((i, x, y, 0.0))
+ return starts
+
+ def _get_win_ends(self, tiles):
+ # Returns a list of ending coordinates for the tiles in the unit
+ # square.
+ ends = []
+ for (i, (x, y)) in enumerate(tiles):
+ ends.append((i + 8, x, y, 1.0))
+ return ends
+
+ def _get_win_length(self):
+ # Returns the length of the win animation based on the existing
+ # values for start and end (in "ticks").
+ return (len(self._win_starts) + 8)
+
+ def _recalc_game_animation_frames(self):
+ if not self._board_is_valid():
+ self._animation_frames = {}
+ return
+
+ frames = {}
+ value_map = self._board.get_value_map()
+ lengths = {}
+
+ # Calculate starting coords.
+ starting_frame = []
+ for ((i, j), value) in value_map.items():
+ starting_frame.append((i, j, 1.0, value))
+ frames[ANIMATE_NONE] = (self._board_transform, starting_frame)
+ lengths[ANIMATE_NONE] = 0.0
+
+ # Calculate shrinking coords.
+ shrinking_frame = []
+ for (i, j, scale, value) in starting_frame:
+ if (i, j) in self._removal_block_set:
+ shrinking_frame.append((i, j, 0.0, value))
+ else:
+ shrinking_frame.append((i, j, scale, value))
+ frames[ANIMATE_SHRINK] = (self._board_transform, shrinking_frame)
+ if len(self._removal_block_set) > 0:
+ lengths[ANIMATE_SHRINK] = 1.0
+ else:
+ lengths[ANIMATE_SHRINK] = 0.0
+
+ # Calculate falling coords.
+ falling_frame = []
+ board2 = self._board.clone()
+ board2.clear_pieces(self._removal_block_set)
+ drop_map = board2.get_drop_map()
+ max_change = 0
+ for (i, j, scale, value) in shrinking_frame:
+ coord = drop_map.get((i, j), None)
+ if coord is None:
+ falling_frame.append((i, j, scale, value))
+ else:
+ falling_frame.append((coord[0], coord[1], scale, value))
+ max_change = max(max_change, j - coord[1])
+ frames[ANIMATE_FALL] = (self._board_transform, falling_frame)
+ if max_change > 0:
+ lengths[ANIMATE_FALL] = 1.0
+ else:
+ lengths[ANIMATE_FALL] = 0.0
+
+ # Calculate sliding coords.
+ sliding_frame = []
+ board2.drop_pieces()
+ slide_map = board2.get_slide_map()
+ max_change = 0
+ for(i, j, scale, value) in falling_frame:
+ if i in slide_map:
+ sliding_frame.append((slide_map[i], j, scale, value))
+ max_change = max(max_change, i - slide_map[i])
+ else:
+ sliding_frame.append((i, j, scale, value))
+ frames[ANIMATE_SLIDE] = (self._board_transform, sliding_frame)
+ if max_change > 0:
+ lengths[ANIMATE_SLIDE] = 1.0
+ else:
+ lengths[ANIMATE_SLIDE] = 0.0
+
+ # Calculate zooming coords.
+ zooming_frame = sliding_frame
+ board2.remove_empty_columns()
+ board_width2 = board2.width
+ board_height2 = board2.height
+ if (board_width2 == self._board_width
+ and board_height2 == self._board_height):
+ zooming_transform = self._board_transform
+ lengths[ANIMATE_ZOOM] = 0.0
+ else:
+ (width, height) = self.window.get_size()
+ zooming_transform = _BoardTransform()
+ zooming_transform.setup(width,
+ height,
+ board_width2,
+ board_height2)
+ lengths[ANIMATE_ZOOM] = 1.0
+ frames[ANIMATE_ZOOM] = (zooming_transform, zooming_frame)
+
+ self._animation_frames = frames
+ self._animation_lengths = lengths
+
+ def _recalc_animation_coords(self):
+ if self._animation_mode == ANIMATE_WIN:
+ self._recalc_win_animation_coords()
+ self._invalidate_board() # XXX Limit to win animation?
+ elif self._animation_mode == ANIMATE_NONE or not self._board_is_valid():
+ self._animation_coords = []
+ else:
+ self._recalc_game_animation_coords()
+ self._invalidate_board()
+
+ def _recalc_win_animation_coords(self):
+ clamped_percent = max(0.0, min(1.0, self._animation_percent))
+ t = clamped_percent * self._win_length
+ coords = []
+ for i in range(len(self._win_starts)):
+ (s_time, s_x, s_y, s_scale) = self._win_starts[i]
+ (e_time, e_x, e_y, e_scale) = self._win_ends[i]
+ delta_time = e_time - s_time
+ w = max(0.0, min(1.0, (t - s_time) / delta_time))
+ inv_w = (1.0 - w)
+ x = s_x * inv_w + e_x * w
+ y = s_y * inv_w + e_y * w
+ scale = s_scale * inv_w + e_scale * w
+ coords.append((x, y, scale))
+ self._win_coords = coords
+
+ def _recalc_game_animation_coords(self):
+ modes = [ANIMATE_NONE,
+ ANIMATE_SHRINK,
+ ANIMATE_FALL,
+ ANIMATE_SLIDE,
+ ANIMATE_ZOOM]
+ mode = self._animation_mode
+ prev_mode = modes[modes.index(mode, 1) - 1]
+
+ w = float(min(max(self._animation_percent, 0.0), 1.0))
+ inv_w = (1.0 - w)
+ (start_transform, start_coords) = self._animation_frames[prev_mode]
+ (end_transform, end_coords ) = self._animation_frames[mode]
+
+ if start_coords is end_coords:
+ self._animation_coords = start_coords
+ else:
+ coords = []
+ for i in range(len(start_coords)):
+ (x1, y1, s1, color1) = start_coords[i]
+ (x2, y2, s2, color2) = end_coords[i]
+ x = (x1 * inv_w + x2 * w)
+ y = (y1 * inv_w + y2 * w)
+ s = (s1 * inv_w + s2 * w)
+ coords.append((x, y, s, color1))
+ self._animation_coords = coords
+
+ if start_transform is end_transform:
+ self._board_transform = start_transform
+ else:
+ self._board_transform.tween(start_transform, end_transform, w)
+
+ @_log_errors
+ def do_expose_event(self, event):
+ cr = self.window.cairo_create()
+ cr.rectangle(event.area.x,
+ event.area.y,
+ event.area.width,
+ event.area.height)
+ cr.clip()
+ (width, height) = self.window.get_size()
+ self._draw(cr, width, height)
+
+ @_log_errors
+ def do_size_allocate(self, allocation):
+ super(GridWidget, self).do_size_allocate(self, allocation)
+ self._init_board_layout(allocation.width, allocation.height)
+
+ def _init_board_layout(self, width, height):
+ if not self._board_is_valid():
+ self._board_transform = _BoardTransform()
+ else:
+ self._board_transform = _BoardTransform()
+ self._board_transform.setup(width,
+ height,
+ self._board_width,
+ self._board_height)
+ self._recalc_win_transform(width, height)
+
+ def _recalc_win_transform(self, width, height):
+ if self._win_size == (0, 0):
+ return
+ self._win_transform = _BoardTransform()
+ self._win_transform.setup(width,
+ height,
+ self._win_size[0],
+ self._win_size[1])
+
+ def _draw(self, cr, width, height):
+ # Draws the widget.
+
+ cr.set_source_rgb(*_BG_COLOR)
+ cr.rectangle(0, 0, width, height)
+ cr.fill()
+
+ cr.save()
+ self._board_transform.set_up_cairo(cr)
+ if self._animation_mode == ANIMATE_NONE:
+ self._draw_board(cr)
+ elif self._animation_mode in (ANIMATE_SHRINK,
+ ANIMATE_FALL,
+ ANIMATE_SLIDE,
+ ANIMATE_ZOOM):
+ self._animate_board(cr)
+ cr.restore()
+
+ if self._win_draw_flag:
+ cr.save()
+ self._win_transform.set_up_cairo(cr)
+ self._draw_win(cr)
+ cr.restore()
+ elif self._animation_mode == ANIMATE_WIN:
+ cr.save()
+ self._win_transform.set_up_cairo(cr)
+ self._draw_animated_win(cr)
+ cr.restore()
+
+ def _animate_board(self, cr):
+ self._animate_blocks(cr)
+
+ def _draw_board(self, cr):
+ # Draws the game board on the widget, where each unit corresponds to
+ # a cell on the board.
+ self._draw_blocks(cr)
+ self._draw_selected(cr)
+ self._draw_selected_dot(cr)
+
+ def _draw_win(self, cr):
+ for (time, x, y, scale) in self._win_ends:
+ if scale > 0.0:
+ self._draw_scaled_block(cr, x, y, self._win_color, scale)
+
+ def _draw_animated_win(self, cr):
+ value = 1
+ for (x, y, scale) in self._win_coords:
+ if scale > 0.0:
+ self._draw_scaled_block(cr, x, y, self._win_color, scale)
+
+ def _animate_blocks(self, cr):
+ for (x, y, scale, value) in self._animation_coords:
+ if scale > 0.0:
+ self._draw_scaled_block(cr, x, y, value, scale)
+
+ def _draw_blocks(self, cr):
+ if not self._board_is_valid():
+ return
+
+ value_map = self._board.get_value_map()
+ for (coord, value) in value_map.items():
+ self._draw_block(cr, coord[0], coord[1], value)
+
+ def _draw_selected(self, cr):
+ # Draws a white background to selected blocks, then redraws blocks
+ # on top.
+ if (self._selected_cell is None
+ or self._selected_cell not in self._contiguous_map):
+ return
+ contiguous = self._contiguous_map[self._selected_cell]
+ value = self._board.get_value(*self._selected_cell)
+ cr.set_source_rgb(*_SELECTED_COLOR)
+ for (x, y) in contiguous:
+ self._draw_square(cr, x, y, _SELECTED_MARGIN)
+ for (x, y) in contiguous:
+ self._draw_block(cr, x, y, value)
+
+ def _draw_block(self, cr, x, y, value):
+ # Draws the block at the given grid cell.
+ assert value is not None
+ c = color.colors[value]
+ cr.set_source_rgb(*c)
+ self._draw_square(cr, x, y, -_BLOCK_GAP)
+
+ def _draw_scaled_block(self, cr, x, y, value, scale):
+ c = color.colors[value]
+ cr.set_source_rgb(*c)
+ inset = 0.5 + scale * (_BLOCK_GAP - 0.5)
+ self._draw_square(cr, x, y, -inset)
+
+ def _draw_square(self, cr, x, y, margin):
+ # Draws a square in the given grid cell with the given margin.
+ x1 = float(x) - margin
+ y1 = float(y) - margin
+ size = 1.0 + margin * 2
+ cr.rectangle(x1, y1, size, size)
+ cr.fill()
+
+ def _draw_selected_dot(self, cr):
+ if self._selected_cell is None:
+ return
+ # Draws a dot indicating the selected cell.
+ cr.set_source_rgb(*_SELECTED_COLOR)
+
+ (x, y) = self._selected_cell
+ cr.arc(x + 0.5, y + 0.5, _SELECTED_DOT_RADIUS, 0, math.pi * 2.0)
+ cr.fill()
+
+ def _recalc_board_dimensions(self):
+ if self._board_is_valid():
+ self._board_width = self._board.width
+ self._board_height = self._board.height
+ else:
+ self._board_width = 1
+ self._board_height = 1
+
+ def _board_is_valid(self):
+ # Returns True if the board is set and has valid dimensions (>=1).
+ return (self._board is not None
+ and not self._board.is_empty())
+
+class _BoardTransform(object):
+ def __init__(self):
+ self.scale_x = 1
+ self.scale_y = 1
+ self.offset_x = 0
+ self.offset_y = 0
+
+ def set_up_cairo(self, cr):
+ cr.translate(self.offset_x,
+ self.offset_y)
+ cr.scale(self.scale_x,
+ self.scale_y)
+
+ def tween(self, trans1, trans2, w):
+ inv_w = 1.0 - w
+ self.scale_x = trans1.scale_x * inv_w + trans2.scale_x * w
+ self.scale_y = trans1.scale_y * inv_w + trans2.scale_y * w
+ self.offset_x = trans1.offset_x * inv_w + trans2.offset_x * w
+ self.offset_y = trans1.offset_y * inv_w + trans2.offset_y * w
+
+ def setup(self, width, height, cells_across, cells_down):
+ if cells_across == 0 or cells_down == 0:
+ self.scale_x = 1
+ self.scale_y = 1
+ self.offset_x = 0
+ self.offset_y = 0
+ return
+
+ border = min(float(width) * _BORDER, float(height) * _BORDER)
+ internal_width = width - border * 2
+ internal_height = height - border * 2
+
+ scale_x = float(internal_width) / cells_across
+ scale_y = float(internal_height) / cells_down
+
+ scale = min(scale_x, scale_y)
+
+ self.scale_x = scale
+ self.scale_y = -scale
+ self.offset_x = (width - cells_across * scale) / 2
+ self.offset_y = height - (height - cells_down * scale) / 2
+
+ def transform(self, x, y):
+ x1 = int(float(x) * self.scale_x + self.offset_x)
+ y1 = int(float(y) * self.scale_y + self.offset_y)
+ return (x1, y1)
+
+ def inverse_transform(self, x, y):
+ x1 = int((float(x) - self.offset_x) / self.scale_x)
+ y1 = int((float(y) - self.offset_y) / self.scale_y)
+ return (x1, y1)
+
+def _interleave(*args):
+ # From Richard Harris' recipe:
+ # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/511480
+ for idx in range(0, max(len(arg) for arg in args)):
+ for arg in args:
+ try:
+ yield arg[idx]
+ except IndexError:
+ continue
diff --git a/icons/easy-level.svg b/icons/easy-level.svg
new file mode 100755
index 0000000..d863d4f
--- /dev/null
+++ b/icons/easy-level.svg
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://web.resource.org/cc/"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ x="0px"
+ y="0px"
+ width="55px"
+ height="55px"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.43"
+ sodipodi:docname="easy-level.svg"
+ sodipodi:docbase="C:\src\implode\icons">
+ <metadata
+ id="metadata9">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs7" />
+ <sodipodi:namedview
+ inkscape:window-height="540"
+ inkscape:window-width="756"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="1"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#7f7f7f"
+ id="base"
+ inkscape:zoom="6.8"
+ inkscape:cx="30.441176"
+ inkscape:cy="33.653437"
+ inkscape:window-x="88"
+ inkscape:window-y="88"
+ inkscape:current-layer="svg2" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:4.00988102;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 6.5343528,30.950261 C 10.072483,30.950261 13.610614,30.950261 17.148744,30.950261 C 17.148744,34.357351 17.148744,37.764441 17.148744,41.171531 C 13.610614,41.171531 10.072483,41.171531 6.5343528,41.171531 C 6.5343528,37.764441 6.5343528,34.357351 6.5343528,30.950261 z "
+ id="rect1310" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:4.00988102;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 22.505079,30.950261 C 26.043211,30.950261 29.581341,30.950261 33.119473,30.950261 C 33.119473,34.357351 33.119473,37.764441 33.119473,41.171531 C 29.581341,41.171531 26.043211,41.171531 22.505079,41.171531 C 22.505079,37.764441 22.505079,34.357351 22.505079,30.950261 z "
+ id="path2359" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:4.00988102;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 38.475807,30.950261 C 42.013937,30.950261 45.552069,30.950261 49.090201,30.950261 C 49.090201,34.357351 49.090201,37.764441 49.090201,41.171531 C 45.552069,41.171531 42.013937,41.171531 38.475807,41.171531 C 38.475807,37.764441 38.475807,34.357351 38.475807,30.950261 z "
+ id="path2357" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:4.00988102;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 38.475807,15.290761 C 42.013937,15.290761 45.552069,15.290761 49.090201,15.290761 C 49.090201,18.697849 49.090201,22.104937 49.090201,25.512025 C 45.552069,25.512025 42.013937,25.512025 38.475807,25.512025 C 38.475807,22.104937 38.475807,18.697849 38.475807,15.290761 z "
+ id="path2353" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:4.00988102;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 22.505079,15.290761 C 26.043211,15.290761 29.581341,15.290761 33.119473,15.290761 C 33.119473,18.697849 33.119473,22.104937 33.119473,25.512025 C 29.581341,25.512025 26.043211,25.512025 22.505079,25.512025 C 22.505079,22.104937 22.505079,18.697849 22.505079,15.290761 z "
+ id="path2345" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:4.00988102;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 6.5343528,15.290761 C 10.072483,15.290761 13.610614,15.290761 17.148744,15.290761 C 17.148744,18.697849 17.148744,22.104937 17.148744,25.512025 C 13.610614,25.512025 10.072483,25.512025 6.5343528,25.512025 C 6.5343528,22.104937 6.5343528,18.697849 6.5343528,15.290761 z "
+ id="path2341" />
+</svg>
diff --git a/icons/edit-redo.svg b/icons/edit-redo.svg
new file mode 100755
index 0000000..8950dec
--- /dev/null
+++ b/icons/edit-redo.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="55px"
+ height="55px" viewBox="0 0 55 55" enable-background="new 0 0 55 55" xml:space="preserve">
+
+<g id="Redo" >
+ <g display="inline">
+ <polyline fill="none" stroke="#FFFFFF" stroke-width="2.9867" stroke-linecap="round" stroke-linejoin="round" points="
+ 33.067,27.523 40.879,20.935 33.067,14.344 "/>
+ <path fill="none" stroke="#FFFFFF" stroke-width="2.9867" stroke-linecap="round" d="M40.879,20.935H23.625
+ c-4.693,0-8.534,3.841-8.534,8.534s3.841,8.533,8.534,8.533h15.548"/>
+ </g>
+</g>
+
+</svg>
diff --git a/icons/edit-undo.svg b/icons/edit-undo.svg
new file mode 100755
index 0000000..d6590e8
--- /dev/null
+++ b/icons/edit-undo.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="55px"
+ height="55px" viewBox="0 0 55 55" enable-background="new 0 0 55 55" xml:space="preserve">
+
+<g id="Undo" >
+ <g display="inline">
+ <polyline fill="none" stroke="#FFFFFF" stroke-width="2.9867" stroke-linecap="round" stroke-linejoin="round" points="
+ 22.903,27.523 15.091,20.935 22.903,14.344 "/>
+ <path fill="none" stroke="#FFFFFF" stroke-width="2.9867" stroke-linecap="round" d="M15.091,20.935h17.254
+ c4.693,0,8.534,3.841,8.534,8.534s-3.841,8.533-8.534,8.533H16.798"/>
+ </g>
+</g>
+</svg>
diff --git a/icons/hard-level.svg b/icons/hard-level.svg
new file mode 100755
index 0000000..19e8494
--- /dev/null
+++ b/icons/hard-level.svg
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://web.resource.org/cc/"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ x="0px"
+ y="0px"
+ width="55px"
+ height="55px"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.43"
+ sodipodi:docname="hard-level.svg"
+ sodipodi:docbase="C:\src\implode\icons">
+ <metadata
+ id="metadata9">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs7" />
+ <sodipodi:namedview
+ inkscape:window-height="540"
+ inkscape:window-width="756"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="1"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#7f7f7f"
+ id="base"
+ inkscape:zoom="6.8"
+ inkscape:cx="30.441176"
+ inkscape:cy="33.653437"
+ inkscape:window-x="88"
+ inkscape:window-y="88"
+ inkscape:current-layer="svg2" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 5.6922254,36.085589 C 7.7442485,36.085589 9.7962717,36.085589 11.848295,36.085589 C 11.848295,38.061612 11.848295,40.037635 11.848295,42.013658 C 9.7962717,42.013658 7.7442485,42.013658 5.6922254,42.013658 C 5.6922254,40.037635 5.6922254,38.061612 5.6922254,36.085589 z "
+ id="rect1310" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 14.954829,36.085589 C 17.006853,36.085589 19.058876,36.085589 21.1109,36.085589 C 21.1109,38.061612 21.1109,40.037635 21.1109,42.013658 C 19.058876,42.013658 17.006853,42.013658 14.954829,42.013658 C 14.954829,40.037635 14.954829,38.061612 14.954829,36.085589 z "
+ id="path2359" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 24.217434,36.085589 C 26.269457,36.085589 28.321481,36.085589 30.373505,36.085589 C 30.373505,38.061612 30.373505,40.037635 30.373505,42.013658 C 28.321481,42.013658 26.269457,42.013658 24.217434,42.013658 C 24.217434,40.037635 24.217434,38.061612 24.217434,36.085589 z "
+ id="path2357" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 33.480038,36.085589 C 35.532061,36.085589 37.584085,36.085589 39.636108,36.085589 C 39.636108,38.061612 39.636108,40.037635 39.636108,42.013658 C 37.584085,42.013658 35.532061,42.013658 33.480038,42.013658 C 33.480038,40.037635 33.480038,38.061612 33.480038,36.085589 z "
+ id="path2355" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 24.217434,27.003488 C 26.269457,27.003488 28.321481,27.003488 30.373505,27.003488 C 30.373505,28.97951 30.373505,30.955532 30.373505,32.931554 C 28.321481,32.931554 26.269457,32.931554 24.217434,32.931554 C 24.217434,30.955532 24.217434,28.97951 24.217434,27.003488 z "
+ id="path2353" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 24.217434,17.921386 C 26.269457,17.921386 28.321481,17.921386 30.373505,17.921386 C 30.373505,19.897408 30.373505,21.87343 30.373505,23.849452 C 28.321481,23.849452 26.269457,23.849452 24.217434,23.849452 C 24.217434,21.87343 24.217434,19.897408 24.217434,17.921386 z "
+ id="path2351" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 33.480038,27.003488 C 35.532061,27.003488 37.584085,27.003488 39.636108,27.003488 C 39.636108,28.97951 39.636108,30.955532 39.636108,32.931554 C 37.584085,32.931554 35.532061,32.931554 33.480038,32.931554 C 33.480038,30.955532 33.480038,28.97951 33.480038,27.003488 z "
+ id="path2349" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 33.480038,17.921386 C 35.532061,17.921386 37.584085,17.921386 39.636108,17.921386 C 39.636108,19.897408 39.636108,21.87343 39.636108,23.849452 C 37.584085,23.849452 35.532061,23.849452 33.480038,23.849452 C 33.480038,21.87343 33.480038,19.897408 33.480038,17.921386 z "
+ id="path2347" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 14.954829,27.003488 C 17.006853,27.003488 19.058876,27.003488 21.1109,27.003488 C 21.1109,28.97951 21.1109,30.955532 21.1109,32.931554 C 19.058876,32.931554 17.006853,32.931554 14.954829,32.931554 C 14.954829,30.955532 14.954829,28.97951 14.954829,27.003488 z "
+ id="path2345" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 14.954829,17.921386 C 17.006853,17.921386 19.058876,17.921386 21.1109,17.921386 C 21.1109,19.897408 21.1109,21.87343 21.1109,23.849452 C 19.058876,23.849452 17.006853,23.849452 14.954829,23.849452 C 14.954829,21.87343 14.954829,19.897408 14.954829,17.921386 z "
+ id="path2343" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 5.6922254,27.003488 C 7.7442485,27.003488 9.7962717,27.003488 11.848295,27.003488 C 11.848295,28.97951 11.848295,30.955532 11.848295,32.931554 C 9.7962717,32.931554 7.7442485,32.931554 5.6922254,32.931554 C 5.6922254,30.955532 5.6922254,28.97951 5.6922254,27.003488 z "
+ id="path2341" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 5.6922254,17.921386 C 7.7442485,17.921386 9.7962717,17.921386 11.848295,17.921386 C 11.848295,19.897408 11.848295,21.87343 11.848295,23.849452 C 9.7962717,23.849452 7.7442485,23.849452 5.6922254,23.849452 C 5.6922254,21.87343 5.6922254,19.897408 5.6922254,17.921386 z "
+ id="path2339" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 42.742643,36.085589 C 44.794666,36.085589 46.84669,36.085589 48.898713,36.085589 C 48.898713,38.061612 48.898713,40.037635 48.898713,42.013658 C 46.84669,42.013658 44.794666,42.013658 42.742643,42.013658 C 42.742643,40.037635 42.742643,38.061612 42.742643,36.085589 z "
+ id="path2337" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 42.742643,27.003488 C 44.794666,27.003488 46.84669,27.003488 48.898713,27.003488 C 48.898713,28.97951 48.898713,30.955532 48.898713,32.931554 C 46.84669,32.931554 44.794666,32.931554 42.742643,32.931554 C 42.742643,30.955532 42.742643,28.97951 42.742643,27.003488 z "
+ id="path2335" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 42.742643,17.921386 C 44.794666,17.921386 46.84669,17.921386 48.898713,17.921386 C 48.898713,19.897408 48.898713,21.87343 48.898713,23.849452 C 46.84669,23.849452 44.794666,23.849452 42.742643,23.849452 C 42.742643,21.87343 42.742643,19.897408 42.742643,17.921386 z "
+ id="path2333" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 24.217434,8.8392837 C 26.269457,8.8392837 28.321481,8.8392837 30.373505,8.8392837 C 30.373505,10.815305 30.373505,12.791328 30.373505,14.76735 C 28.321481,14.76735 26.269457,14.76735 24.217434,14.76735 C 24.217434,12.791328 24.217434,10.815305 24.217434,8.8392837 z "
+ id="path2331" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 33.480038,8.8392837 C 35.532061,8.8392837 37.584085,8.8392837 39.636108,8.8392837 C 39.636108,10.815305 39.636108,12.791328 39.636108,14.76735 C 37.584085,14.76735 35.532061,14.76735 33.480038,14.76735 C 33.480038,12.791328 33.480038,10.815305 33.480038,8.8392837 z "
+ id="path2329" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 14.954829,8.8392837 C 17.006853,8.8392837 19.058876,8.8392837 21.1109,8.8392837 C 21.1109,10.815305 21.1109,12.791328 21.1109,14.76735 C 19.058876,14.76735 17.006853,14.76735 14.954829,14.76735 C 14.954829,12.791328 14.954829,10.815305 14.954829,8.8392837 z "
+ id="path2327" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 5.6922254,8.8392837 C 7.7442485,8.8392837 9.7962717,8.8392837 11.848295,8.8392837 C 11.848295,10.815305 11.848295,12.791328 11.848295,14.76735 C 9.7962717,14.76735 7.7442485,14.76735 5.6922254,14.76735 C 5.6922254,12.791328 5.6922254,10.815305 5.6922254,8.8392837 z "
+ id="path2325" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2.32562613;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 42.742643,8.8392837 C 44.794666,8.8392837 46.84669,8.8392837 48.898713,8.8392837 C 48.898713,10.815305 48.898713,12.791328 48.898713,14.76735 C 46.84669,14.76735 44.794666,14.76735 42.742643,14.76735 C 42.742643,12.791328 42.742643,10.815305 42.742643,8.8392837 z "
+ id="path2323" />
+</svg>
diff --git a/icons/medium-level.svg b/icons/medium-level.svg
new file mode 100755
index 0000000..f852916
--- /dev/null
+++ b/icons/medium-level.svg
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://web.resource.org/cc/"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ x="0px"
+ y="0px"
+ width="55px"
+ height="55px"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.43"
+ sodipodi:docname="medium-level.svg"
+ sodipodi:docbase="C:\src\implode\icons">
+ <metadata
+ id="metadata9">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs7" />
+ <sodipodi:namedview
+ inkscape:window-height="540"
+ inkscape:window-width="756"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="1"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#7f7f7f"
+ id="base"
+ inkscape:zoom="6.8"
+ inkscape:cx="27.5"
+ inkscape:cy="26.741673"
+ inkscape:window-x="88"
+ inkscape:window-y="88"
+ inkscape:current-layer="svg2" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 6.0294123,34.029411 C 8.6764711,34.029411 11.32353,34.029411 13.970589,34.029411 C 13.970589,36.578431 13.970589,39.127452 13.970589,41.676472 C 11.32353,41.676472 8.6764711,41.676472 6.0294123,41.676472 C 6.0294123,39.127452 6.0294123,36.578431 6.0294123,34.029411 z "
+ id="rect1310" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 17.990196,34.029411 C 20.637255,34.029411 23.284314,34.029411 25.931374,34.029411 C 25.931374,36.578431 25.931374,39.127452 25.931374,41.676472 C 23.284314,41.676472 20.637255,41.676472 17.990196,41.676472 C 17.990196,39.127452 17.990196,36.578431 17.990196,34.029411 z "
+ id="path2240" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 29.950981,34.029411 C 32.59804,34.029411 35.245099,34.029411 37.892159,34.029411 C 37.892159,36.578431 37.892159,39.127452 37.892159,41.676472 C 35.245099,41.676472 32.59804,41.676472 29.950981,41.676472 C 29.950981,39.127452 29.950981,36.578431 29.950981,34.029411 z "
+ id="path2238" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 41.911766,34.029411 C 44.558825,34.029411 47.205884,34.029411 49.852943,34.029411 C 49.852943,36.578431 49.852943,39.127452 49.852943,41.676472 C 47.205884,41.676472 44.558825,41.676472 41.911766,41.676472 C 41.911766,39.127452 41.911766,36.578431 41.911766,34.029411 z "
+ id="path2236" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 29.950981,22.264706 C 32.59804,22.264706 35.245099,22.264706 37.892159,22.264706 C 37.892159,24.813725 37.892159,27.362745 37.892159,29.911764 C 35.245099,29.911764 32.59804,29.911764 29.950981,29.911764 C 29.950981,27.362745 29.950981,24.813725 29.950981,22.264706 z "
+ id="path2234" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 29.950981,10.5 C 32.59804,10.5 35.245099,10.5 37.892159,10.5 C 37.892159,13.049019 37.892159,15.598039 37.892159,18.147058 C 35.245099,18.147058 32.59804,18.147058 29.950981,18.147058 C 29.950981,15.598039 29.950981,13.049019 29.950981,10.5 z "
+ id="path2232" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 41.911766,22.264706 C 44.558825,22.264706 47.205884,22.264706 49.852943,22.264706 C 49.852943,24.813725 49.852943,27.362745 49.852943,29.911764 C 47.205884,29.911764 44.558825,29.911764 41.911766,29.911764 C 41.911766,27.362745 41.911766,24.813725 41.911766,22.264706 z "
+ id="path2230" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 41.911766,10.5 C 44.558825,10.5 47.205884,10.5 49.852943,10.5 C 49.852943,13.049019 49.852943,15.598039 49.852943,18.147058 C 47.205884,18.147058 44.558825,18.147058 41.911766,18.147058 C 41.911766,15.598039 41.911766,13.049019 41.911766,10.5 z "
+ id="path2228" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 17.990196,22.264706 C 20.637255,22.264706 23.284314,22.264706 25.931374,22.264706 C 25.931374,24.813725 25.931374,27.362745 25.931374,29.911764 C 23.284314,29.911764 20.637255,29.911764 17.990196,29.911764 C 17.990196,27.362745 17.990196,24.813725 17.990196,22.264706 z "
+ id="path2226" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 17.990196,10.5 C 20.637255,10.5 23.284314,10.5 25.931374,10.5 C 25.931374,13.049019 25.931374,15.598039 25.931374,18.147058 C 23.284314,18.147058 20.637255,18.147058 17.990196,18.147058 C 17.990196,15.598039 17.990196,13.049019 17.990196,10.5 z "
+ id="path2224" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 6.0294123,22.264706 C 8.6764711,22.264706 11.32353,22.264706 13.970589,22.264706 C 13.970589,24.813725 13.970589,27.362745 13.970589,29.911764 C 11.32353,29.911764 8.6764711,29.911764 6.0294123,29.911764 C 6.0294123,27.362745 6.0294123,24.813725 6.0294123,22.264706 z "
+ id="path2222" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 6.0294123,10.5 C 8.6764711,10.5 11.32353,10.5 13.970589,10.5 C 13.970589,13.049019 13.970589,15.598039 13.970589,18.147058 C 11.32353,18.147058 8.6764711,18.147058 6.0294123,18.147058 C 6.0294123,15.598039 6.0294123,13.049019 6.0294123,10.5 z "
+ id="path2220" />
+</svg>
diff --git a/icons/new-game.svg b/icons/new-game.svg
new file mode 100755
index 0000000..5081fbd
--- /dev/null
+++ b/icons/new-game.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ x="0px"
+ y="0px"
+ width="55px"
+ height="55px" >
+<path
+ style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:3.95744967;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 15,47.647058 L 20.357437,29.061607 L 4.7920195,17.579494 L 24.123373,16.931499 L 30.233533,-1.4202612 L 36.823529,16.764707 L 56.165234,16.904809 L 40.906722,28.791732 L 46.750393,47.23008 L 30.730117,36.391634 L 15,47.647058 z "
+ transform="matrix(0.704893,0,0,0.702581,6.494446,10.69787)" /></svg>
diff --git a/icons/replay-game.svg b/icons/replay-game.svg
new file mode 100755
index 0000000..8966bc6
--- /dev/null
+++ b/icons/replay-game.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ x="0px"
+ y="0px"
+ width="55px"
+ height="55px" >
+<path
+ style="fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 23.540935,16.565116 L 23.540935,42.111354 L 8.1151708,29.338235 L 23.540935,16.565116 z " /><path
+ style="fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 42.966713,16.565116 L 42.966713,42.111354 L 27.540948,29.338235 L 42.966713,16.565116 z " /></svg>
diff --git a/implodeactivity.py b/implodeactivity.py
new file mode 100755
index 0000000..09a2974
--- /dev/null
+++ b/implodeactivity.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2007, Joseph C. Lee
+#
+# This program 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 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+_logger = logging.getLogger('implode-activity')
+
+from gettext import gettext as _
+
+from sugar.activity.activity import Activity, ActivityToolbox
+from sugar.graphics.toolbutton import ToolButton
+from sugar.graphics.radiotoolbutton import RadioToolButton
+
+import implodegame
+
+#import sys, os
+import gtk
+import gobject
+
+_EASY = 0
+_MEDIUM = 1
+_HARD = 2
+
+class ImplodeActivity(Activity):
+ def hello(self, widget, data=None):
+ logging.info("Hello World")
+
+ def __init__(self, handle):
+ super(ImplodeActivity, self).__init__(handle)
+
+ _logger.debug('Starting implode activity...')
+
+ self._game = implodegame.ImplodeGame()
+
+ toolbox = _Toolbox(self)
+ self.set_toolbox(toolbox)
+ toolbox.show()
+
+ for (signal, func) in (('new-game-clicked' , self._game.new_game),
+ ('replay-game-clicked', self._game.replay_game),
+ ('undo-clicked' , self._game.undo),
+ ('redo-clicked' , self._game.redo)):
+ def callback(source, func=func):
+ func()
+ toolbox.connect(signal, callback)
+
+ for (signal, level) in (('easy-clicked' , 0),
+ ('medium-clicked', 1),
+ ('hard-clicked' , 2)):
+ def callback(source, level=level):
+ self._game.set_level(level)
+ toolbox.connect(signal, callback)
+
+ self.set_canvas(self._game)
+ self.show_all()
+
+class _Toolbox(ActivityToolbox):
+ __gsignals__ = {
+ 'new-game-clicked' : (gobject.SIGNAL_RUN_LAST, None, ()),
+ 'replay-game-clicked': (gobject.SIGNAL_RUN_LAST, None, ()),
+ 'undo-clicked' : (gobject.SIGNAL_RUN_LAST, None, ()),
+ 'redo-clicked' : (gobject.SIGNAL_RUN_LAST, None, ()),
+ 'easy-clicked' : (gobject.SIGNAL_RUN_LAST, None, ()),
+ 'medium-clicked' : (gobject.SIGNAL_RUN_LAST, None, ()),
+ 'hard-clicked' : (gobject.SIGNAL_RUN_LAST, None, ()),
+ }
+
+ def __init__(self, activity):
+ super(_Toolbox, self).__init__(activity)
+
+ toolbar = gtk.Toolbar()
+
+ def add_button(icon_name, tooltip, signal_name):
+ button = ToolButton(icon_name)
+ toolbar.add(button)
+
+ def callback(source):
+ self.emit(signal_name)
+ button.connect('clicked', callback)
+ button.set_tooltip(tooltip)
+
+ return button
+
+ add_button('new-game' , "New" , 'new-game-clicked')
+ add_button('replay-game', "Replay", 'replay-game-clicked')
+ add_button('edit-undo' , "Undo" , 'undo-clicked')
+ add_button('edit-redo' , "Redo" , 'redo-clicked')
+
+ toolbar.add(gtk.SeparatorToolItem())
+
+ levels = []
+ def add_level_button(icon_name, tooltip, signal_name):
+ if levels:
+ button = RadioToolButton(icon_name, levels[0])
+ else:
+ button = RadioToolButton(icon_name)
+ levels.append(button)
+ toolbar.add(button)
+
+ def callback(source):
+ if source.get_active():
+ self.emit(signal_name)
+ button.connect('clicked', callback)
+ button.set_tooltip(tooltip)
+
+ add_level_button('easy-level' , "Easy" , 'easy-clicked')
+ add_level_button('medium-level', "Medium", 'medium-clicked')
+ add_level_button('hard-level' , "Hard" , 'hard-clicked')
+
+ self.add_toolbar('Game', toolbar)
+ self.set_current_toolbar(1)
+
diff --git a/implodegame.py b/implodegame.py
new file mode 100755
index 0000000..d629bb6
--- /dev/null
+++ b/implodegame.py
@@ -0,0 +1,207 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2007, Joseph C. Lee
+#
+# This program 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 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+_logger = logging.getLogger('implode-activity.implodegame')
+
+from gettext import gettext as _
+
+import gobject
+import gtk
+import random
+import time
+
+import board
+import boardgen
+import gridwidget
+
+# A list of the animation stages in order, along with time on-screen (in
+# seconds per tick).
+_ANIM_TIME_LIST = (
+ (gridwidget.ANIMATE_SHRINK, 0.1),
+ (gridwidget.ANIMATE_FALL, 0.1),
+ (gridwidget.ANIMATE_SLIDE, 0.1),
+ (gridwidget.ANIMATE_ZOOM, 0.1),
+)
+_ANIM_MODES = [x[0] for x in _ANIM_TIME_LIST]
+_ANIM_TIMES = dict(_ANIM_TIME_LIST)
+
+# Win animation time on screen (in seconds per tick).
+_WIN_ANIM_TIME = 0.04
+
+# Animation timer interval (in msec)
+_TIMER_INTERVAL = 20
+
+class ImplodeGame(gtk.EventBox):
+ """Gtk widget for playing the implode game."""
+
+ def __init__(self, *args, **kwargs):
+ super(ImplodeGame, self).__init__(*args, **kwargs)
+ self._animate = True
+ self._animation_mode = gridwidget.ANIMATE_NONE
+ self._start_time = 0.0
+
+ self._board = None
+ self._undoStack = []
+ self._redoStack = []
+
+ self._random = random.Random()
+ #self._random.seed(0)
+ self._difficulty = 0
+ self._size = (8, 6)
+ self._contiguous = None
+ self._seed = 0
+ self._fragmentation = 0
+
+ self._grid = gridwidget.GridWidget()
+ self._grid.connect('piece-selected', self._piece_selected_cb)
+ self.add(self._grid)
+
+ self.new_game()
+
+ def new_game(self):
+ _logger.debug('New game.')
+ print "New game"
+ self._seed = self._random.randint(0, 99999)
+ size_frag_dict = {
+ 0: (( 8, 6), 0),
+ 1: ((12, 10), 0),
+ 2: ((20, 15), 2),
+ }
+ (self._size, self._fragmentation) = size_frag_dict[self._difficulty]
+ self._reset_board()
+
+ def replay_game(self):
+ print "Replay game"
+ _logger.debug('Replay game.')
+ self._reset_board()
+
+ def undo(self):
+ print "Undo"
+ _logger.debug('Undo.')
+ if len(self._undoStack) == 0:
+ return
+
+ self._redoStack.append(self._board)
+ self._board = self._undoStack.pop()
+
+ # Force board refresh.
+ self._grid.set_board(self._board)
+ self._grid.set_win_draw_flag(False)
+
+ def redo(self):
+ print "Redo"
+ _logger.debug('Redo.')
+ if len(self._redoStack) == 0:
+ return
+
+ self._undoStack.append(self._board)
+ self._board = self._redoStack.pop()
+
+ # Force board refresh.
+ self._grid.set_board(self._board)
+
+ def set_level(self, level):
+ self._difficulty = level
+
+ def _reset_board(self):
+ # Regenerates the board with the current seed.
+ self._board = boardgen.generate_board(seed=self._seed,
+ fragmentation=self._fragmentation,
+ max_size=self._size)
+ self._grid.set_board(self._board)
+ self._grid.set_win_draw_flag(False)
+ self._undoStack = []
+ self._redoStack = []
+
+ def _piece_selected_cb(self, widget, x, y):
+ # Handles piece selection.
+ contiguous = self._board.get_contiguous(x, y)
+ if len(contiguous) >= 3:
+ self._contiguous = contiguous
+ if not self._animate:
+ self._remove_contiguous()
+ else:
+ gobject.timeout_add(_TIMER_INTERVAL, self._removal_timer)
+ self._start_time = time.time()
+ self._animation_mode = 0
+ self._grid.set_removal_block_set(contiguous)
+ self._grid.set_animation_mode(_ANIM_MODES[0])
+ self._grid.set_animation_percent(0.0)
+
+ def _remove_contiguous(self):
+ self._redoStack = []
+ self._undoStack.append(self._board.clone())
+ self._board.clear_pieces(self._contiguous)
+ self._board.drop_pieces()
+ self._board.remove_empty_columns()
+
+ # Force board refresh.
+ self._grid.set_board(self._board)
+
+ if self._board.is_empty():
+ if not self._animate:
+ self._init_win_state()
+ else:
+ gobject.timeout_add(_TIMER_INTERVAL, self._win_timer)
+ self._start_time = time.time()
+ self._grid.set_animation_mode(gridwidget.ANIMATE_WIN)
+ self._grid.set_animation_percent(0.0)
+ else:
+ contiguous = self._board.get_all_contiguous()
+ if len(contiguous) == 0:
+ self._init_lose_state()
+
+ def _init_win_state(self):
+ self._grid.set_win_draw_flag(True)
+
+ def _init_lose_state(self):
+ pass
+
+ def _win_timer(self):
+ delta = time.time() - self._start_time
+ total = _WIN_ANIM_TIME * self._grid.get_animation_length()
+ if total > 0:
+ percent = float(delta) / total
+ if percent < 1.0:
+ self._grid.set_animation_percent(percent)
+ return True
+ self._grid.set_animation_mode(gridwidget.ANIMATE_NONE)
+ self._init_win_state()
+ return False
+
+ def _removal_timer(self):
+ delta = time.time() - self._start_time
+ total = (_ANIM_TIMES[_ANIM_MODES[self._animation_mode]]
+ * self._grid.get_animation_length())
+ if total > 0:
+ percent = float(delta) / total
+ if percent < 1.0:
+ self._grid.set_animation_percent(percent)
+ return True
+ self._animation_mode += 1
+ if self._animation_mode >= len(_ANIM_MODES):
+ self._grid.set_animation_mode(gridwidget.ANIMATE_NONE)
+ self._remove_contiguous()
+ return False
+ else:
+ self._grid.set_animation_mode(_ANIM_MODES[self._animation_mode])
+ self._grid.set_animation_percent(0.0)
+ self._start_time = time.time()
+ return True
+
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..4159284
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,5 @@
+#!/usr/bin/env python
+
+from sugar.activity import bundlebuilder
+if __name__ == "__main__":
+ bundlebuilder.start("ImplodeActivity")
diff --git a/sugarless.py b/sugarless.py
new file mode 100755
index 0000000..4fa460f
--- /dev/null
+++ b/sugarless.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2007, Joseph C. Lee
+#
+# This program 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 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+# A stub file for running the application on a sugarless GTK, when the Activity
+# framework is not available.
+
+import pygtk
+pygtk.require('2.0')
+import gtk
+
+import implodegame
+
+class ImplodeWindow(gtk.Window):
+ def __init__(self):
+ super(ImplodeWindow, self).__init__(gtk.WINDOW_TOPLEVEL)
+ self.set_geometry_hints(None, min_width=640, min_height=480)
+ self.set_title("Implode")
+
+ self.connect("delete_event", self._delete_event_cb)
+
+ toolbar = gtk.Toolbar()
+ self.game = implodegame.ImplodeGame()
+
+ def add_button(id, func):
+ button = gtk.ToolButton(id)
+ toolbar.add(button)
+
+ def callback(source):
+ func()
+ button.connect('clicked', callback)
+
+ return button
+
+ add_button(gtk.STOCK_NEW, self.game.new_game)
+ add_button(gtk.STOCK_MEDIA_PREVIOUS, self.game.replay_game)
+ add_button(gtk.STOCK_UNDO, self.game.undo)
+ add_button(gtk.STOCK_REDO, self.game.redo)
+
+ toolbar.add(gtk.SeparatorToolItem())
+
+ radio_buttons = []
+ def add_radio_button(label, func):
+ button = gtk.RadioToolButton()
+ button.set_label(label)
+ toolbar.add(button)
+ radio_buttons.append(button)
+
+ def callback(source):
+ if source.get_active():
+ func()
+ button.connect('clicked', callback)
+
+ return button
+
+ add_radio_button('easy', self._easy_clicked)
+ add_radio_button('medium', self._medium_clicked)
+ add_radio_button('hard', self._hard_clicked)
+ for button in radio_buttons[1:]:
+ button.set_group(radio_buttons[0])
+
+ main_box = gtk.VBox(False, 0)
+ main_box.pack_start(toolbar, False)
+ main_box.pack_start(self.game, True, True, 0)
+ self.add(main_box)
+
+ self.show_all()
+
+ def _delete_event_cb(self, window, event):
+ gtk.main_quit()
+ return False
+
+ def _easy_clicked(self):
+ print "Easy"
+
+ def _medium_clicked(self):
+ print "Medium"
+
+ def _hard_clicked(self):
+ print "Hard"
+
+
+def main():
+ w = ImplodeWindow()
+ gtk.main()
+
+if __name__ == "__main__":
+ main()