PageSourceSearch

https://mathsanswers.org.uk/investigations/polyominoes/polyominoes.py

js mathsanswers.org.uk collected 2026-09-25 19:48:35 UTC 27,671 bytes, 611 lines download raw bytes

1import time, random
2tt = time.time()
3from browser import document, window, html
4from browser.timer import set_timeout, request_animation_frame
5#print("brython imports", time.time()-tt)
6import brySVG.polygoncanvas as SVG
7#print("brySVG", time.time()-tt)
8import brywidgets as ws
9#print("brywidgets", time.time()-tt)
10from puzzledata import puzzles
11from ommydata import piecedata, pieceorder
12#print("puzzledata", time.time()-tt)
13
14ws.Global.alertStyle = None
15rotatelefticon = SVG.svg.path(d="M 0,8 a 8,8 0 1 0 -5.66,-13.66 l 0,-4 m 0,4 l 4,1", style={"stroke":"black", "strokeWidth":2, "fill":"none", "strokeLinejoin":"round", "strokeLinecap":"round"})
16rotaterighticon = SVG.svg.path(d="M 0,8 a 8,8 0 1 1 5.66,-13.66 l 0,-4 m 0,4 l -4,1", style={"stroke":"black", "strokeWidth":2, "fill":"none", "strokeLinejoin":"round", "strokeLinecap":"round"})
17reflectXicon = SVG.GroupObject([SVG.PolygonObject([(-10,0),(-10,4),(-2,4),(-2,-4),(-6,-4),(-6,0)]),SVG.PolygonObject([(10,0),(10,4),(2,4),(2,-4),(6,-4),(6,0)]),SVG.LineObject([(0,-10),(0,10)],style="faintdash2", linewidth=2)])
18reflectYicon = SVG.GroupObject([SVG.PolygonObject([(0,-10),(4,-10),(4,-2),(-4,-2),(-4,-6),(0,-6)]),SVG.PolygonObject([(0,10),(4,10),(4,2),(-4,2),(-4,6),(0,6)]),SVG.LineObject([(-10,0),(10,0)],style="faintdash2", linewidth=2)])
19
20tcombine = [
21[0,1,2,3,4,5,6,7],
22[1,2,3,0,6,7,5,4],
23[2,3,0,1,5,4,7,6],
24[3,0,1,2,7,6,4,5],
25[4,7,5,6,0,2,3,1],
26[5,6,4,7,2,0,1,3],
27[6,4,7,5,1,3,0,2],
28[7,5,6,4,3,1,2,0]
29]
30
31tinverse = [0,3,2,1,4,5,6,7]
32
33class AddPieceDialog(ws.DialogBox):
34    def __init__(self):
35        super().__init__("Check for solution")
36        self <= html.IMG(src="tick.png")
37        self <= html.P("Success - the puzzle can be solved from this position.")
38        self <= html.P("Would you like any extra pieces placed as a hint?")
39        self.hintcount = ws.SpinControl(1, 1, 5)
40        self <= html.P(["Number of pieces:", self.hintcount])
41        addbutton = ws.Button("Place extra pieces", self.onaddbutton)
42        closebutton = ws.Button("No thanks", self.close)
43        self <= html.P([addbutton, closebutton])
44
45    def onaddbutton(self, event):
46        self.hide()
47        puzzlecanvas.addpieces(self.hintcount.currentvalue)
48
49class RemovePieceDialog(ws.DialogBox):
50    def __init__(self):
51        super().__init__("Check for solution")
52        self <= html.IMG(src="cross.png")
53        self <= html.P("Unfortunately the puzzle cannot be solved from this position.")
54        self <= html.P("Would you like some pieces removed in order to get back to a position from which it can be solved?")
55        self.removebutton = ws.Button("Remove pieces", self.onremovebutton)
56        closebutton = ws.Button("No thanks", self.close)
57        self <= html.P([self.removebutton, closebutton])
58        self.hide()
59
60    def onremovebutton(self, event):
61        self.removebutton.innerHTML = "Please wait"
62        do_after_page_refresh(puzzlecanvas.removepieces)
63
64class ComplexPolygon(SVG.GroupObject):
65    def __init__(self, polygons=[[(0, 0)]], holes=[], linecolour="black", linewidth=1, fillcolour="yellow", holecolour="white", objid=None):
66        self._polygons = [SVG.PolygonObject(pointlist, linecolour, linewidth, fillcolour) for pointlist in polygons]
67        self._holes = [SVG.PolygonObject(pointlist, linecolour, linewidth, holecolour) for pointlist in holes]
68        self.linecolour = linecolour
69        self.linewidth = linewidth
70        self.fillcolour = fillcolour
71        self.holecolour = holecolour
72        super().__init__(self._polygons+self._holes)
73        if objid: self.id = objid
74
75    @property
76    def polygons(self):
77        return self._polygons
78
79    @polygons.setter
80    def polygons(self, polygons):
81        for poly in self._polygons: self.removeObject(poly)
82        self._polygons = [SVG.PolygonObject(pointlist, self.linecolour, self.linewidth, self.fillcolour) for pointlist in polygons]
83        self.addObjects(self._polygons)
84
85    @property
86    def holes(self):
87        return self._holes
88
89    @holes.setter
90    def holes(self, holes):
91        for poly in self._holes: self.removeObject(poly)
92        self._holes = [SVG.PolygonObject(pointlist, self.linecolour, self.line
92width, self.holecolour) for pointlist in holes]
93        self.addObjects(self._holes)
94
95    def getBoundingBox(self):
96        allpoints = sum((poly.pointList for poly in self.polygons), start=[])
97        return SVG._getboundingbox(allpoints)
98
99class Polyomino(SVG.PolygonObject):
100    def __init__(self, coords=None, offsetcoords=None, colour=None, number=None, startmatrix=None, transformlist=None):
101        super().__init__()
102        self.coords = coords
103        self.offsetcoords = offsetcoords
104        self.colour = colour
105        self.number = number
106        self.startmatrix = startmatrix
107        self.transformlist = transformlist
108        self.currenttransform = 0
109        self.transforms = [self.identity, self.rotateleft, self.rotate180, self.rotateright, self.reflectX, self.reflectY, self.reflectXleft, self.reflectYleft]
110        self.csstransforms = ["", "rotate(-90deg)", "rotate(180deg)", "rotate(90deg)", "scale(-1, 1)", "scale(1, -1)", "scale(-1, 1) rotate(-90deg)", "scale(1, -1) rotate(-90deg)"]
111        #The following attributes are only set once a puzzle has been chosen
112        self.startcoords = self.startcx = self.startcy = self.fixed = None
113        self.matrix = self.mwidth = self.mheight = self.offsets = None
114
115    def reset(self):
116        self.setPointList(self.startcoords)
117        self.matrix = self.startmatrix[:]
118        self.updateoffsets()
119        self.currenttransform =  0
120        self.style.transition = "none"
121        self.style.transform = "translate(0px,0px)"
122        self.style.opacity = 1
123
124    def identity(self):
125        pass
126
127    def rotateleft(self):
128        self.rotate(-90, self.getCentre())
129        self.matrix = [[self.matrix[j][self.mwidth-1-i] for j in range(self.mheight)] for i in range(self.mwidth)]
130        self.updateoffsets()
131        self.currenttransform = tcombine[self.currenttransform][1]
132
133    def rotate180(self):
134        self.rotate(180, self.getCentre())
135        self.matrix = list(reversed([list(reversed(row)) for row in self.matrix]))
136        self.updateoffsets()
137        self.currenttransform = tcombine[self.currenttransform][2]
138
139    def rotateright(self):
140        self.rotate(90, self.getCentre())
141        self.matrix = [[self.matrix[self.mheight-1-j][i] for j in range(self.mheight)] for i in range(self.mwidth)]
142        self.updateoffsets()
143        self.currenttransform = tcombine[self.currenttransform][3]
144
145    def reflectX(self):
146        (cx, cy) = self.getCentre()
147        self.xstretch(-1, cx)
148        self.matrix = [list(reversed(row)) for row in self.matrix]
149        self.updateoffsets()
150        self.currenttransform = tcombine[self.currenttransform][4]
151
152    def reflectY(self):
153        (cx, cy) = self.getCentre()
154        self.ystretch(-1, cy)
155        self.matrix.reverse()
156        self.updateoffsets()
157        self.currenttransform = tcombine[self.currenttransform][5]
158
159    def reflectXleft(self):
160        self.rotateleft()
161        self.reflectX()
162
163    def reflectYleft(self):
164        self.rotateleft()
165        self.reflectY()
166
167    def updateoffsets(self):
168        gridheight = puzzlecanvas.gridheight
169        offsets = []
170        for i, row in enumerate(self.matrix):
171            for j, square in enumerate(row):
172                if square == 0: offsets.append(j*gridheight+i)
173        offsets.sort()
174        self.offsets = offsets
175        self.mwidth = len(self.matrix[0])
176        self.mheight = len(self.matrix)
177
178    def gettransformedstartcoords(self, transformno):
179        clone = self.cloneObject()
180        clone.setPointList(self.startcoords)
181        clone.matrix = self.startmatrix[:]
182        clone.updateoffsets()
183        clone.transforms[transformno]()
184        return min(clone.pointList)
185
186class PuzzleCanvas(SVG.CanvasObject):
187    def __init__(self):
188        super().__init__(colour="cyan", objid="puzzlecanvas")
189        self.xcentre, buttontop, unit = 5450, 5300, 8
190        bwidth, ibwidth, bheight, spacer = 8*unit, 3*unit, 3*unit, 2*unit
191        bleft = self.xcentre-29*unit
192        self.pieceareatop = buttontop+5*unit
193        bdata = [("Instructions", self.instructions), ("Choose\nNew Puzzle", self.onmenuclick),
194        (rotatelefticon, self.rotateleftclick), (rotaterighticon, self.rotaterightclick),
195        (reflectXicon, self.reflectXclick), (reflectYicon, self.reflectYclick),
196        ("Restart", self.restartpuzzle), ("Check /\nGet Hint", self.onhintclick)]
197        for (bcontent, onclick) in bdata:
198            if isinstance(bcontent, str):
199                self <= SVG.Button((bleft, buttontop), (bwidth, bheight), bcontent, onclick, fontsize=9, fillcolour="pink")
200                bleft += (bwidth+spacer)
201            else:
202                self <= SVG.ImageButton((bleft, buttontop), (ibwidth, bheight), bcontent, onclick, fillcolour="pink")
203                bleft += (ibwidth+spacer)
204        self.puzzleoutline = ComplexPolygon(fillcolour="white", holecolour="cyan")
205        self.addObject(self.puzzleoutline, fixed=True)
206
207        self.colours = ["peru", "pink", "orange", "blue", "magenta", "lime", "khaki", "yellow", "palegreen", "teal", "darkorchid", "silver"]*3
208
209        self.pieces = []
210        for i, pieceinfo in enumerate(piecedata[ommysize]):
211            piece = Polyomino(pieceinfo["coords"], pieceinfo["offset"], self.colours[i], i+1, pieceinfo["matrix"], pieceinfo["transforms"])
212            self.addObject(piece)
213            piece.bind("mousedown", self.onpiececlick)
214            piece.bind("touchstart", self.onpiececlick)
215            self.pieces.append(piece)
216
217        self.selectedpiece = None
218        self.bind("contextmenu", self.onRightClick)
219        self.bind("mouseup", self.onMouseUp)
220        self.bind("touchend", self.onMouseUp)
221        self.style.display="none"
222        self.mouseMode = SVG.MouseMode.DRAG
223
224    def showpuzzle(self, puzzleno):
225        self.style.display = "block"
226        puzzle = puzzles[ommysize][puzzleno]
227        for key in puzzle: setattr(self, key, puzzle[key])
228        if self.piecestouse != []:
229            self.allowedpieces = [p for p in self.piecestouse if p not in self.fixedpieces]
230        else:
231            self.allowedpieces = [p for p in pieceorder[ommysize] if p not in self.fixedpieces]
232        self.solution = None
233
234        if self.gridheight > self.gridwidth:
235            self.partscoords = [[(y, x) for (x, y) in poly] for poly in self.partscoords]
236            self.holescoords = [[(y, x) for (x, y) in hole] for hole in self.holescoords]
237            self.gridlist = sum([self.gridlist[k:len(self.gridlist):self.gridheight] for k in range(self.gridheight)], [])
238            self.gridwidth, self.gridheight = self.gridheight, self.gridwidth
239        self.puzzleoutline.polygons = self.partscoords
240        self.puzzleoutline.holes = self.holescoords
241        self.outlineleft = self.xcentre-self.gridwidth*15/2
242        self.outlinetop = self.positionpieces()
243        self.puzzleoutline.translate((self.outlineleft,self.outlinetop))
244        self.fitContents()
245
246    def positionpieces(self):
247        if self.piecestouse:
248            (boxwidth, boxheight) = (90, 60)
249            colcount = 5
250            (xstart, ystart) = (self.xcentre-colcount*boxwidth/2, self.pieceareatop)
251            usedcount = 0
252            for piece in self.pieces:
253                if piece.number in self.piecestouse:
254                    row, col = divmod(usedcount, colcount)
255                    piece.startcoords = [(x+col*boxwidth+xstart, y+row*boxheight+ystart) for (x, y) in piece.coords]
256                    piece.style.display = "inline"
257                    usedcount += 1
258                else:
259                    piece.style.display = "none"
260            outlinetop = ystart + ((usedcount-1)//colcount + 1)*boxheight
261        else:
262            (pieceareawidth, pieceareaheight) = (270, 75) if ommysize==4 else (300, 105) if ommysize==5 else (465, 255)
263            (xstart, ystart) = (self.xcentre-pieceareawidth/2, self.pieceareatop)
264            for piece in self.pieces:
265                (xoffset, yoffset) = piece.offsetcoords
266                piece.startcoords = [(x+xoffset+xstart, y+yoffset+ystart) for (x,y) in piece.coords]
267                piece.style.display = "inline"
268            outlinetop = self.pieceareatop + pieceareaheight + 15
269        self.restartpuzzle()
270        return outlinetop
271
272    def restartpuzzle(self, event=None):
273        for piece in self.pieces:
274            if self.piecestouse and (piece.number not in self.piecestouse): continue
275            piece.reset()
276            (piece.startcx, piece.startcy) = piece.getCentre()
277            if piece.number in self.fixedpieces:
278                piece.fixed = True
279                piece.style.fill = "white"
280                piece.style.strokeDasharray = 3
281            else:
282                piece.fixed = False
283                piece.style.fill = piece.colour
284                piece.style.strokeDasharray = "none"
285
286        self.usedpieces = []
287        self.gridlist = [(99 if x==99 else 0) for x in self.gridlist]
288
289    def instructions(self, event):
290        ws.showalert ("""Fill the shape using the pieces.  For some puzzles you do not need to use all the pieces.
291If there is a piece which is white with a dashed border, it cannot be used for this puzzle.
292
293To rotate a piece through 90 degrees,
294or reflect it vertically or horizontally,
295select it and then click on the appropriate button.
296Alternatively, right-click on a shape to rotate it clockwise.""", "How to play")
297
298    def onmenuclick(self, event):
299        self.style.display = "none"
300        menucanvas.style.display = "block"
301
302    def onhintclick(self, event):
303        if len(self.usedpieces) == self.piececount: return
304        pleasewaitdialog.show()
305        do_after_page_refresh(self.checkposition)
306
307    def onRightClick(self, event):
308        event.preventDefault()
309        if event.target in self.pieces:
310            self.objectDict[event.target.id].rotateright()
311            set_timeout(self.checkmove, 0)
312
313    def onpiececlick(self, event):
314        event.preventDefault()
315        for piece in self.pieces:
316            piece.style.stroke = "black"
317        self.selectedpiece = self.objectDict[event.target.id]
318        self.selectedpiece.style.stroke = "red"
319
320    def rotateleftclick(self, event):
321        if self.selectedpiece:
322            self.selectedpiece.rotateleft()
323            set_timeout(self.checkmove, 0)
324
325    def rotaterightclick(self, event):
326        if self.selectedpiece:
327            self.selectedpiece.rotateright()
328            set_timeout(self.checkmove, 0)
329
330    def reflectXclick(self, event):
331        if self.selectedpiece:
332            self.selectedpiece.reflectX()
333            set_timeout(self.checkmove, 0)
334
335    def reflectYclick(self, event):
336        if self.selectedpiece:
337            self.selectedpiece.reflectY()
338            set_timeout(self.checkmove, 0)
339
340    def onMouseUp(self, event):
341        if event.type == "mouseup" and event.button > 0: return
342        if self.getSelectedObject(event.target.id) not in self.pieces: return
343        if not self.selectedpiece: return
344        set_timeout(self.checkmove, 0)
345
346    def checkmove(self):
347        piece = self.selectedpiece
348        piece.style.opacity = 1
349        pieceno = piece.number
350        self.gridlist = [(0 if x==pieceno else x) for x in self.gridlist] #If piece was in grid, remove it before replacing
351
352        (x, y) = min(piece.pointList)
353        (i, j1) = (int((x-self.outlineleft)/15+0.4), int((y-self.outlinetop)/15+0.4))
354        j = j1-piece.offsets[0]
355        if i>=0 and i+piece.mwidth <= self.gridwidth and j>=0 and j+piece.mheight <= self.gridheight:
356            startsquare = i*self.gridheight+j
357            for offset in self.selectedpiece.offsets:
358                if self.gridlist[startsquare+offset] != 0:
359                    piece.style.opacity = 0.5
360                    break
361            else:
362                for offset in self.selectedpiece.offsets: self.gridlist[startsquare+offset] = pieceno
363                (x1, y1) = (self.outlineleft+i*15, self.outlinetop+j1*15)
364                piece.translate((x1-x, y1-y))
365                if pieceno not in self.usedpieces: self.usedpieces.append(pieceno)
366                #print("Placed by hand:")
367                #print("\n".join([str(self.gridlist[k:len(self.gridlist):self.gridheight]) for k in range(self.gridheight)]))
368                #print("Current used pieces:", self.usedpieces)
369                if (len(self.usedpieces) == self.piececount) and (0 not in self.gridlist):
370                    self.selectedpiece.style.stroke = "black"
371                    self.cyclecolours(iter(self.colours[:8]))
372        else:
373            try:
374                self.usedpieces.remove(pieceno)
375                #print("Piece removed from grid:")
376                #print("\n".join([str(self.gridlist[k:len(self.gridlist):self.gridheight]) for k in range(self.gridheight)]))
377                #print("Current used pieces:", self.usedpieces)
378            except ValueError: pass
379
380    def cyclecolours(self, colourlist):
381        def changecolour():
382            nonlocal colour
383            for piece in self.pieces:
384                piece.style.fill = colour
385                piece.style.strokeDasharray = "none"
386            try:
387                colour = next(colourlist)
388            except:
389                return
390            set_timeout(changecolour,100)
391        colour = next(colourlist)
392        set_timeout(changecolour,0)
393
394    def checkposition(self):
395        def issubgrid(partialgrid, fullgrid):
396            for m, n in zip(partialgrid, fullgrid):
397                if m>0 and m!=n: return False
398            return True
399
400        tt = time.time()
401        success = False
402        if self.solution:
403            (finalgrid, finalused, finaltlist) = self.solution
404            if issubgrid(self.gridlist, finalgrid):
405                #print("Can use previous solution.")
406                success = True
407                finalused = self.usedpieces + [p for p in finalused if p not in self.usedpieces]
408                self.solution = (finalgrid, finalused, finaltlist)
409        if not success:
410            success, finalgrid, finalused, tlist = window.solvepuzzle(ommysize, self.gridwidth, self.gridheight, self.gridlist, self.allowedpieces, self.usedpieces)
411            if success and (0 not in finalgrid): self.storesolution(finalgrid, finalused, tlist)
412        #print(time.time() - tt)
413        pleasewaitdialog.hide()
414        if success and (0 in finalgrid):
415            ws.showalert("Sorry - it is taking too long to check this position.\nYou could add or remove some pieces, and then try again", "Check for solution")
416            #timeoutdialog.show()
417        elif success:
418            addpiecedialog.show()
419            #print("Solution:")
420            #print("\n".join([str(finalgrid[k:len(finalgrid):self.gridheight]) for k in range(self.gridheight)]))
421            #print("Order placed:", finalused)
422            #print("Transform list:", [(i,finaltlist[i]) for i in finalused])
423        else:
424            removepiecedialog.show()
425            #print("No solution")
426
427    def storesolution(self, finalgrid, finalused, tlist):
428        finaltlist  = [0]*(len(self.pieces)+1)
429        for pieceno in finalused:
430            piece = self.pieces[pieceno-1]
431            if pieceno in self.usedpieces:
432                finaltlist[pieceno] = piece.currenttransform
433            else:
434                finaltlist[pieceno] = piece.transformlist[tlist[pieceno]]
435        self.solution = (finalgrid, finalused, finaltlist)
436
437    def addpieces(self, n):
438        def addpiece(pieceno, requiredtransform, newsquare, newposition):
439            piece = self.pieces[pieceno-1]
440            piece.reset()
441            piece.transforms[requiredtransform]()
442            (x0, y0) = min(piece.pointList)
443            (x, y) = newposition
444            piece.translate((x-x0, y-y0))
445
446            startsquare = newsquare - piece.offsets[0]
447            for offset in piece.offsets: self.gridlist[startsquare+offset] = pieceno
448            self.usedpieces.append(pieceno)
449            #print("After hint:")
450            #print("\n".join([str(self.gridlist[k:len(self.gridlist):self.gridheight]) for k in range(self.gridheight)]))
451            #print("Current used pieces:", self.usedpieces)
452
453        (finalgrid, finalused, finaltlist) = self.solution
454        piecesleft = finalused[len(self.usedpieces):]
455        #print("Pieces left:", piecesleft)
456        if n > len(piecesleft): n = len(piecesleft)
457        piecelist = random.sample(piecesleft, n)
458        #print("Pieces to place", piecelist)
459        for pieceno in piecelist:
460            piece = self.pieces[pieceno-1]
461
462            inversetransform = tinverse[piece.currenttransform]
463            requiredtransform = finaltlist[pieceno]
464            combinedtransform = tcombine[inversetransform][requiredtransform]
465
466            (cx, cy) = piece.getCentre()
467            (dx, dy) = (cx-piece.startcx, cy-piece.startcy)
468            (x0, y0) = piece.gettransformedstartcoords(requiredtransform)
469            square = finalgrid.index(pieceno)
470            (i, j) = divmod(square, self.gridheight)
471            (x, y) = (self.outlineleft+i*15, self.outlinetop+j*15)
472
473            piece.style.transition = "transform 1s"
474            piece.style.transformOrigin = f"{cx}px {cy}px"
475            piece.style.transform = f"translate({x-x0-dx}px,{y-y0-dy}px) " + piece.csstransforms[combinedtransform]
476
477            set_timeout(addpiece, 1000, pieceno, requiredtransform, square, (x, y))
478
479    def removepieces(self):
480        def removepiece(pieceno):
481            piece = self.pieces[pieceno-1]
482            piece.reset()
483
484        piecesremoved = []
485        success = False
486        while not success:
487            pieceno = self.usedpieces.pop()
488            piecesremoved.append(pieceno)
489            #print("Removed so far:", piecesremoved)
490            self.gridlist = [(0 if x==pieceno else x) for x in self.gridlist]
491            #print("After removal:")
492            #print("\n".join([str(self.gridlist[k:len(self.gridlist):self.gridheight]) for k in range(self.gridheight)]))
493            success, finalgrid, finalused, tlist = window.solvepuzzle(ommysize, self.gridwidth, self.gridheight, self.gridlist, self.allowedpieces, self.usedpieces)
494        removepiecedialog.removebutton.innerHTML = "Remove pieces"
495        removepiecedialog.hide()
496
497        for pieceno in piecesremoved:
498            piece = self.pieces[pieceno-1]
499            (cx, cy) = piece.getCentre()
500            tno = piece.currenttransform
501            (x0, y0) = piece.gettransformedstartcoords(tno)
502            (x, y) = min(piece.pointList)
503
504            piece.style.transition = "transform 1s"
505            piece.style.transformOrigin = f"{cx}px {cy}px"
506            piece.style.transform = f" translate({x0-x}px,{y0-y}px)" + piece.csstransforms[tinverse[tno]]
507            set_timeout(removepiece, 1000, pieceno)
508
509        if 0 in finalgrid:
510            ws.showalert("Sorry - some pieces have been removed but this new position is taking too long to check.\nYou may still need to remove more pieces")
511        else:
512            self.storesolution(finalgrid, finalused, tlist)
513
514class MenuCanvas(SVG.CanvasObject):
515    def __init__(self):
516        self.bgcolour = "cyan"
517        super().__init__(colour=self.bgcolour, objid="menucanvas")
518        self.style.display = "none"
519        self.pageno = 0
520
521    def displaypage(self, pagenumber):
522        boxsize = 250
523        colcount, rowcount = 5, 2
524        perpage = colcount*rowcount
525        textheight = boxsize*0.1
526        maxshapesize = boxsize - textheight*2.5
527        self.style.display = "block"
528        start, end = perpage*pagenumber, perpage*(pagenumber+1)
529        for i, puzzle in enumerate(puzzles[ommysize][start:end]):
530            xoffset, yoffset = i%colcount*boxsize, i//colcount*boxsize
531            shape = ComplexPolygon(puzzle["partscoords"], puzzle["holescoords"], objid="puzzle"+str(perpage*pagenumber+i))
532            shape.attrs["cursor"] = "pointer"
533            shape.bind("click", self.choosepuzzle)
534            shape.bind("touchstart", self.choosepuzzle)
535            (xmin, ymin), (xmax, ymax) = shape.getBoundingBox()
536            width, height = xmax-xmin, ymax-ymin
537            maxdim = max(width, height)
538
539            self.addObject(shape, fixed=True)
540            if maxdim > maxshapesize: shape.enlarge(maxshapesize/maxdim, ((xmin+xmax)/2, ymax))
541            shape.translate((xoffset-xmin+(boxsize-width)/2, yoffset+(boxsize-textheight*1.5-ymax)))
542            title = SVG.TextObject(puzzle["description"], (xoffset+boxsize/2, yoffset+boxsize), anchorposition=8, fontsize=textheight)
543            self <= title
544        heading = "Choose a puzzle by clicking on one of the shapes below."
545        self <= SVG.TextObject(heading, (boxsize*colcount/2, 0), anchorposition=8, fontsize=textheight*1.2)
546
547        nextpageicon = SVG.PolylineObject([(-2,-8), (2,0), (-2,8)], linewidth=2)
548        nextpagebutton = SVG.ImageButton((boxsize*colcount, boxsize*rowcount/2), (boxsize*0.1,boxsize*0.2), nextpageicon, self.shownextpage, fillcolour="pink", canvas=self)
549        self <= nextpagebutton
550        lastpage = (len(puzzles[ommysize])-1)//perpage
551        if self.pageno == lastpage: nextpagebutton.style.visibility = "hidden"
552        prevpageicon = SVG.PolylineObject([(2,-8), (-2,0), (2,8)], linewidth=2)
553        prevpagebutton = SVG.ImageButton((0, boxsize*rowcount/2), (boxsize*0.1,boxsize*0.2), prevpageicon, self.showprevpage, fillcolour="pink", canvas=self)
554        self <= prevpagebutton
555        if self.pageno == 0: prevpagebutton.style.visibility = "hidden"
556        self <= SVG.PointObject((boxsize*colcount, boxsize*rowcount), self.bgcolour, 1)
557        self.fitContents()
558
559    def shownextpage(self, event):
560        self.pageno += 1
561        self.deleteAll()
562        do_after_page_refresh(self.displaypage, self.pageno)
563
564    def showprevpage(self, event):
565        self.pageno -= 1
566        self.deleteAll()
567        do_after_page_refresh(self.displaypage, self.pageno)
568
569    def choosepuzzle(self, event):
570        self.style.display = "none"
571        puzzleno = int(event.currentTarget.id[6:])
572        do_after_page_refresh(puzzlecanvas.showpuzzle, puzzleno)
573
574def do_after_page_refresh(function, *args):
575    def frame1(timestamp):  #Refresh page before function is executed
576        def frame2(timestamp): #Refresh page again after function is executed
577            function(*args)
578        request_animation_frame(frame2)
579    request_animation_frame(frame1)
580
581def onwindowresize(event=None):
582    h = 0.96*window.innerHeight-document['header'].offsetHeight
583    menucanvas.style.height = f"{h}px"
584    puzzlecanvas.style.height = f"{h}px"
585    if puzzlecanvas.style.display == "block": puzzlecanvas.fitContents()
586
587ommysize = document.query.getfirst("size", None)
588if ommysize: ommysize = int(ommysize)
589if not ommysize or ommysize not in {4,5,6}: ommysize = 4
590heading = "Hexomino puzzles" if ommysize==6 else "Pentomino Puzzles" if ommysize==5 else "Tetromino Puzzles"
591document["heading"].text = heading
592
593menucanvas = MenuCanvas()
594puzzlecanvas = PuzzleCanvas()
595document["maindiv"] <= (menucanvas, puzzlecanvas)
596window.bind("resize", onwindowresize)
597onwindowresize()
598
599addpiecedialog = AddPieceDialog()
600removepiecedialog = RemovePieceDialog()
601pleasewaitdialog = ws.DialogBox(title="Checking for solution", content = html.P("Checking for solution - please wait..."))
602#print("Create canvasses and dialogs", time.time()-tt)
603
604puzzleno = document.query.getfirst("puzzle", None)
605if puzzleno:
606    puzzlecanvas.showpuzzle(int(puzzleno))
607else:
608    menucanvas.displaypage(menucanvas.pageno)
609#print("Show puzzle", time.time()-tt)
610
611

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.