diff options
author | Joe Lee <joe@jotaro.com> | 2008-03-11 05:18:54 (GMT) |
---|---|---|
committer | Joe Lee <joe@jotaro.com> | 2008-03-11 05:18:54 (GMT) |
commit | 0044b6872ca1cd38e96c81d7f3ce21c5689b7f0c (patch) | |
tree | f95248f4cb71f17a128bb7903b07342fd4265def |
Initial import
-rwxr-xr-x | MANIFEST | 17 | ||||
-rwxr-xr-x | NEWS | 36 | ||||
-rwxr-xr-x | activity/activity-implode.svg | 41 | ||||
-rwxr-xr-x | activity/activity.info | 7 | ||||
-rwxr-xr-x | board.py | 300 | ||||
-rwxr-xr-x | boardgen.py | 310 | ||||
-rwxr-xr-x | boardgentest.py | 356 | ||||
-rwxr-xr-x | color.py | 48 | ||||
-rwxr-xr-x | gridwidget.py | 699 | ||||
-rwxr-xr-x | icons/easy-level.svg | 72 | ||||
-rwxr-xr-x | icons/edit-redo.svg | 16 | ||||
-rwxr-xr-x | icons/edit-undo.svg | 15 | ||||
-rwxr-xr-x | icons/hard-level.svg | 128 | ||||
-rwxr-xr-x | icons/medium-level.svg | 96 | ||||
-rwxr-xr-x | icons/new-game.svg | 14 | ||||
-rwxr-xr-x | icons/replay-game.svg | 15 | ||||
-rwxr-xr-x | implodeactivity.py | 126 | ||||
-rwxr-xr-x | implodegame.py | 207 | ||||
-rwxr-xr-x | setup.py | 5 | ||||
-rwxr-xr-x | sugarless.py | 102 |
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 @@ -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() |