1
# Copyright 2007 World Wide Workshop Foundation
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17
# If you find this activity useful or end up using parts of it in one of your
18
# own creations we would love to hear from you at info@WorldWideWorkshop.org !
27
from mamamedia_modules import utils
29
#from utils import load_image, calculate_matrix, debug, SliderCreator, trace
31
from types import TupleType, ListType
32
from random import random
35
from cStringIO import StringIO
42
up_key = ['Up', 'KP_Up', 'KP_8']
43
down_key = ['Down', 'KP_Down', 'KP_2']
44
left_key = ['Left', 'KP_Left', 'KP_4']
45
right_key = ['Right', 'KP_Right', 'KP_6']
52
def calculate_matrix (pieces):
53
""" Given a number of pieces, calculate the best fit 2 dimensional matrix """
54
rows = int(sqrt(pieces))
55
cols = int(float(pieces) / rows)
56
return rows*cols, rows, cols
59
class SliderCreator (gtk.gdk.Pixbuf):
60
def __init__ (self, width, height, fname=None, tlist=None): #tlist):
65
super(SliderCreator, self).__init__(gtk.gdk.COLORSPACE_RGB, False, 8, width, height)
68
cmds = file(fname).readlines()
79
self.prepare_stringed(2,2)
81
#def scale_simple (self, w,h,m):
82
# return SliderCreator(w,h,tlist=self.tlist)
84
#def subpixbuf (self, x,y,w,h):
85
# return SliderCreator(w,h,tlist=self.tlist)
88
def can_handle(klass, fname):
89
return fname.lower().endswith('.sequence')
91
def prepare_stringed (self, rows, cols):
92
# We use a Pixmap as offscreen drawing canvas
93
cm = gtk.gdk.colormap_get_system()
94
pm = gtk.gdk.Pixmap(None, self.width, self.height, cm.get_visual().depth)
95
#pangolayout = pm.create_pango_layout("")
96
font_size = int(self.width / cols / 4)
98
pangolayout = pango.Layout(l.create_pango_context())
99
pangolayout.set_font_description(pango.FontDescription("sans bold %i" % font_size))
101
gc.set_colormap(gtk.gdk.colormap_get_system())
102
color = cm.alloc_color('white')
103
gc.set_foreground(color)
104
pm.draw_rectangle(gc, True, 0, 0, self.width, self.height)
105
color = cm.alloc_color('black')
106
gc.set_foreground(color)
108
sw, sh = (self.width / cols), (self.height / rows)
109
item = iter(self.tlist)
110
for r in range(rows):
111
for c in range(cols):
115
# pm.draw_line(gc, px, 0, px, self.height-1)
116
# pm.draw_line(gc, 0, py, self.width-1, py)
117
pangolayout.set_text(str(item.next()))
118
pe = pangolayout.get_pixel_extents()
119
pe = pe[1][2]/2, pe[1][3]/2
120
pm.draw_layout(gc, px + (sw / 2) - pe[0], py + (sh / 2) - pe[1], pangolayout)
121
self.get_from_drawable(pm, cm, 0, 0, 0, 0, -1, -1)
123
utils.register_image_type(SliderCreator)
129
class MatrixPosition (object):
130
""" Helper class to hold a x/y coordinate, and move it by passing a direction,
131
taking care of enforcing boundaries as needed.
132
The x and y coords are 0 based. """
133
def __init__ (self, rowsize, colsize, x=0, y=0):
134
self.rowsize = rowsize
135
self.colsize = colsize
136
self.x = min(x, colsize-1)
137
self.y = min(y, rowsize-1)
139
def __eq__ (self, other):
140
if isinstance(other, (TupleType, ListType)) and len(other) == 2:
141
return self.x == other[0] and self.y == other[1]
144
def __ne__ (self, other):
145
return not self.__eq__ (other)
147
def bottom_right (self):
148
""" Move to the lower right position of the matrix, having 0,0 as the top left corner """
149
self.x = self.colsize - 1
150
self.y = self.rowsize-1
152
def move (self, direction, count=1):
153
""" Moving direction is actually the opposite of what is passed.
154
We are moving the hole position, so if you slice a piece down into the hole,
155
that hole is actually moving up.
156
Returns bool, false if we can't move in the requested direction."""
157
if direction == SLIDE_UP and self.y < self.rowsize-1:
160
if direction == SLIDE_DOWN and self.y > 0:
163
if direction == SLIDE_LEFT and self.x < self.colsize-1:
166
if direction == SLIDE_RIGHT and self.x > 0:
172
return MatrixPosition(self.rowsize, self.colsize, self.x, self.y)
175
return (self.rowsize, self.colsize, self.x, self.y)
177
def _thaw (self, obj):
179
self.rowsize, self.colsize, self.x, self.y = obj
182
class SliderPuzzleMap (object):
183
""" This class holds the game logic.
184
The current pieces position is held in self.pieces_map[YROW][XROW].
186
def __init__ (self, pieces=9, move_cb=None):
188
self.move_cb = move_cb
191
def reset (self, pieces=9):
192
self.pieces, self.rowsize, self.colsize = calculate_matrix(pieces)
193
pieces_map = range(1,self.pieces+1)
195
for i in range(self.rowsize):
196
self.pieces_map.append(pieces_map[i*self.colsize:(i+1)*self.colsize])
197
self.hole_pos = MatrixPosition(self.rowsize, self.colsize)
198
self.hole_pos.bottom_right()
199
self.solved_map = [list(x) for x in self.pieces_map]
200
self.solved_map[-1][-1] = None
202
def randomize (self):
203
""" To make sure the randomization is solvable, we don't simply shuffle the numbers.
204
We move the hole in random directions through a finite number of iteractions. """
205
# Remove the move callback temporarily
209
iteractions = self.rowsize * self.colsize * (int(100*random())+1)
212
for i in range(iteractions):
213
while not (self.do_move(int(4*random())+1)):
218
# Now move the hole to the bottom right
219
for x in range(self.colsize-self.hole_pos.x-1):
220
self.do_move(SLIDE_LEFT)
221
for y in range(self.rowsize-self.hole_pos.y-1):
222
self.do_move(SLIDE_UP)
224
# Put the callback where it was
228
def do_move (self, slide_direction):
230
The moves are relative to the moving piece:
232
>>> jm = SliderPuzzleMap()
237
>>> jm.do_move(SLIDE_DOWN)
239
>>> jm.debug_map() # DOWN
243
>>> jm.do_move(SLIDE_RIGHT)
245
>>> jm.debug_map() # RIGHT
249
>>> jm.do_move(SLIDE_UP)
251
>>> jm.debug_map() # UP
255
>>> jm.do_move(SLIDE_LEFT)
257
>>> jm.debug_map() # LEFT
262
We can't move over the matrix edges:
264
>>> jm.do_move(SLIDE_LEFT)
266
>>> jm.debug_map() # LEFT
270
>>> jm.do_move(SLIDE_UP)
272
>>> jm.debug_map() # UP
276
>>> jm.do_move(SLIDE_RIGHT)
278
>>> jm.do_move(SLIDE_RIGHT)
280
>>> jm.do_move(SLIDE_RIGHT)
282
>>> jm.debug_map() # RIGHT x 3
286
>>> jm.do_move(SLIDE_DOWN)
288
>>> jm.do_move(SLIDE_DOWN)
290
>>> jm.do_move(SLIDE_DOWN)
292
>>> jm.debug_map() # DOWN x 3
297
# What piece are we going to move?
298
old_hole_pos = self.hole_pos.clone()
299
if self.hole_pos.move(slide_direction):
300
# Move was a success, now update the map
301
self.pieces_map[old_hole_pos.y][old_hole_pos.x] = self.pieces_map[self.hole_pos.y][self.hole_pos.x]
303
if self.move_cb is not None:
304
self.move_cb(self.hole_pos.x, self.hole_pos.y, old_hole_pos.x, old_hole_pos.y)
308
def do_move_piece (self, piece):
309
""" Move the piece (1 based index) into the hole, if possible
310
>>> jm = SliderPuzzleMap()
315
>>> jm.do_move_piece(6)
317
>>> jm.debug_map() # Moved 6
321
>>> jm.do_move_piece(2)
323
>>> jm.debug_map() # No move
328
Return True if a move was done, False otherwise.
330
for y in range(self.rowsize):
331
for x in range(self.colsize):
332
if self.pieces_map[y][x] == piece:
333
if self.hole_pos.x == x:
334
if abs(self.hole_pos.y-y) == 1:
335
return self.do_move(self.hole_pos.y > y and SLIDE_DOWN or SLIDE_UP)
336
elif self.hole_pos.y == y:
337
if abs(self.hole_pos.x-x) == 1:
338
return self.do_move(self.hole_pos.x > x and SLIDE_RIGHT or SLIDE_LEFT)
343
def is_hole_at (self, x, y):
345
>>> jm = SliderPuzzleMap()
350
>>> jm.is_hole_at(2,2)
352
>>> jm.is_hole_at(0,0)
355
return self.hole_pos == (x,y)
357
def is_solved (self):
359
>>> jm = SliderPuzzleMap()
360
>>> jm.do_move_piece(6)
364
>>> jm.do_move_piece(6)
369
if self.hole_pos != (self.colsize-1, self.rowsize-1):
371
self.pieces_map[self.hole_pos.y][self.hole_pos.x] = None
372
self.solved = self.pieces_map == self.solved_map
377
def get_cell_at (self, x, y):
378
if x < 0 or x >= self.colsize or y < 0 or y >= self.rowsize or self.is_hole_at(x,y):
380
return self.pieces_map[y][x]
382
def debug_map (self):
383
for y in range(self.rowsize):
384
for x in range(self.colsize):
385
if self.hole_pos == (x,y):
388
print self.pieces_map[y][x],
395
return {'pieces': self.pieces, 'rowsize': self.rowsize, 'colsize': self.colsize,
396
'pieces_map': self.pieces_map, 'hole_pos_freeze': self.hole_pos._freeze()}
398
def _thaw (self, obj):
401
setattr(self, k, obj[k])
402
self.hole_pos._thaw(obj.get('hole_pos_freeze', None))
409
class SliderPuzzleWidget (gtk.Table):
410
__gsignals__ = {'solved' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
411
'shuffled' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
412
'moved' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),}
414
def __init__ (self, pieces=9, width=480, height=480):
415
self.jumbler = SliderPuzzleMap(pieces, self.jumblermap_piece_move_cb)
416
# We take this from the jumbler object because it may have altered our requested value
417
gtk.Table.__init__(self, self.jumbler.rowsize, self.jumbler.colsize)
418
self.image = None #gtk.Image()
421
self.set_size_request(width, height)
424
def prepare_pieces (self):
425
""" set up a list of UI objects that will serve as pieces, ordered correctly """
427
if self.image is None:
428
# pb = self.image.get_pixbuf()
429
#if self.image is None or pb is None:
430
for i in range(self.jumbler.pieces):
431
self.pieces.append(gtk.Button(str(i+1)))
432
self.pieces[-1].connect("button-release-event", self.process_mouse_click, i+1)
433
self.pieces[-1].show()
435
if isinstance(self.image, SliderCreator):
436
# ask for image creation
437
self.image.prepare_stringed(self.jumbler.rowsize, self.jumbler.colsize)
439
w = self.image.get_width() / self.jumbler.colsize
440
h = self.image.get_height() / self.jumbler.rowsize
441
for y in range(self.jumbler.rowsize):
442
for x in range(self.jumbler.colsize):
444
img.set_from_pixbuf(self.image.subpixbuf(x*w, y*h, w-1, h-1))
446
self.pieces.append(gtk.EventBox())
447
self.pieces[-1].add(img)
448
self.pieces[-1].connect("button-press-event", self.process_mouse_click, (y*self.jumbler.colsize)+x+1)
449
self.pieces[-1].show()
450
self.set_row_spacings(1)
451
self.set_col_spacings(1)
454
def full_refresh (self):
456
self.foreach(self.remove)
457
self.prepare_pieces()
458
# Add the pieces in their respective places
459
for y in range(self.jumbler.rowsize):
460
for x in range(self.jumbler.colsize):
461
pos = self.jumbler.get_cell_at(x, y)
463
self.attach(self.pieces[pos-1], x, x+1, y, y+1)
465
def process_mouse_click (self, b, e, i):
466
# i is the 1 based index of the piece
467
self.jumbler.do_move_piece(i)
469
def process_key (self, w, e):
470
if self.get_parent() == None:
472
k = gtk.gdk.keyval_name(e.keyval)
474
self.jumbler.do_move(SLIDE_UP)
477
self.jumbler.do_move(SLIDE_DOWN)
480
self.jumbler.do_move(SLIDE_LEFT)
483
self.jumbler.do_move(SLIDE_RIGHT)
487
### SliderPuzzleMap specific callbacks ###
489
def jumblermap_piece_move_cb (self, hx, hy, px, py):
490
if not hasattr(self, 'pieces'):
492
piece = self.pieces[self.jumbler.get_cell_at(px, py)-1]
494
self.attach(piece, px, px+1, py, py+1)
496
if self.jumbler.solved:
499
### Parent callable interface ###
501
def get_nr_pieces (self):
502
return self.jumbler.pieces
505
def set_nr_pieces (self, nr_pieces):
506
self.jumbler.reset(nr_pieces)
507
self.resize(self.jumbler.rowsize, self.jumbler.colsize)
511
def randomize (self):
512
""" Jumble the SliderPuzzle """
513
self.jumbler.randomize()
515
self.emit("shuffled")
518
def load_image (self, image, width=0, height=0):
519
""" Loads an image from the file.
520
width and height are processed as follows:
521
-1 : follow the loaded image size
522
0 : follow the size set on widget instantiation
523
* : use that specific size"""
528
if not isinstance(image, SliderCreator):
529
self.image = utils.resize_image(image, width, height)
535
def set_image (self, image):
540
def set_image_from_str (self, image):
548
self.image = i.get_pixbuf()
551
def show_image (self):
552
""" Shows the full image, used as visual clue for solved puzzle """
554
self.foreach(self.remove)
555
if hasattr(self, 'pieces'):
557
# Resize to a single cell and use that for the image
560
img.set_from_pixbuf(self.image)
561
self.attach(img, 0,1,0,1)
564
def get_image_as_png (self, cb=None):
565
if self.image is None:
571
self.image.save_to_callback(cb, "png")
577
def _freeze (self, journal=True):
578
""" returns a json writable object representation capable of being used to restore our current status """
580
return {'jumbler': self.jumbler._freeze(),
581
'image': self.get_image_as_png(),
584
return {'jumbler': self.jumbler._freeze()}
586
def _thaw (self, obj):
587
""" retrieves a frozen status from a python object, as per _freeze """
589
self.jumbler._thaw(obj['jumbler'])
590
if obj.has_key('image') and obj['image'] is not None:
591
self.set_image_from_str(obj['image'])
599
if __name__ == '__main__':