3
# Martin Heistermann, <mh at sponc dot de>
5
# planarity (aka untangle) is free software: you can redistribute it and/or
6
# modify it under the terms of the GNU General Public License as published
7
# by the Free Software Foundation, either version 3 of the License, or
8
# (at your option) any later version.
10
# planarity is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
15
# You should have received a copy of the GNU General Public License
16
# along with planarity. If not, see <http://www.gnu.org/licenses/>.
18
from libavg import avg, Point2D, AVGApp
19
from libavg.AVGAppUtil import getMediaDir
27
BASE_SIZE = Point2D(1280, 720)
29
g_player = avg.Player.get()
33
def getDelta(motion, topLeft, bottomRight, boundingSize):
34
xDelta = min(max(motion.x, -topLeft.x), boundingSize.x - bottomRight.x)
35
yDelta = min(max(motion.y, -topLeft.y), boundingSize.y - bottomRight.y)
36
return Point2D(xDelta, yDelta)
38
class VertexGroup(object):
39
def __init__(self, gameController, polygon, vertices):
40
self._polygon = g_player.createNode("polygon", {
42
'strokewidth': 3*g_scale,
46
self._vertices = vertices
47
self._gameController = gameController
48
self._gameController.level.addVertexGroup(self)
49
self._gameController.vertexDiv.appendChild(self._polygon)
51
self._button = g_player.createNode('image', {'href': 'close-button.png'})
52
self._gameController.vertexDiv.appendChild(self._button)
53
self._button.size *= g_scale
54
self._button.pos = polygon[0] - self._button.size/2
55
self._button.setEventHandler(avg.CURSORDOWN, avg.TOUCH | avg.MOUSE,
56
lambda event: self.delete())
58
xCoords = [vertex.pos.x for vertex in vertices]
59
yCoords = [vertex.pos.y for vertex in vertices]
60
self.topLeft = Point2D(min(xCoords), min(yCoords)) - vertices[0].size/2
61
self.bottomRight = Point2D(max(xCoords), max(yCoords)) + vertices[0].size/2
64
delta = getDelta(event.motion, self.topLeft, self.bottomRight,
65
self._gameController.vertexDiv.size)
66
for i, vertex in enumerate(self._vertices):
68
self._polygon.pos = [pos + delta for pos in self._polygon.pos]
69
self._button.pos += delta
71
self.bottomRight += delta
73
self._mover = MoveButton(self._polygon, onMotion=onMotion)
77
self._gameController.ungroupVertices(self._vertices)
78
self._polygon.unlink()
81
class GroupDetector(object):
82
"""use this as an event handler"""
83
def __init__(self, gameController, event):
84
self._gameController = gameController
85
self._polyline = g_player.createNode("polyline", {
89
gameController.groupDiv.appendChild(self._polyline)
91
self._cursorid = event.cursorid
92
self._polyline.setEventCapture(self._cursorid)
93
self._polyline.setEventHandler(avg.CURSORMOTION, avg.TOUCH | avg.MOUSE,
95
self._polyline.setEventHandler(avg.CURSORUP, avg.TOUCH | avg.MOUSE,
96
lambda event: self.delete())
101
def getClosedPolygon(self):
102
"""If the last edge intersects any edge, return a cleaned-up polygon
103
representing the enclosed region."""
104
points = self._polyline.pos # in-Python object copy
107
last_edge = points[-2:]
108
for i in range(len(points) - 2):
109
edge = [points[i], points[i + 1]]
110
intersection = line_intersect(edge, last_edge)
112
# include the intersection point itself, plus all the edges
113
# after the intersecting edge, omitting the last edge
114
return [intersection] + points[i + 1:-1]
117
def _onMotion(self, event):
118
self._polyline.pos += [event.pos]
119
polygon = self.getClosedPolygon()
121
vertices = self._gameController.groupVertices(polygon)
124
VertexGroup(self._gameController, polygon, vertices)
127
self._polyline.releaseEventCapture(self._cursorid)
128
self._polyline.setEventHandler(avg.CURSORMOTION, avg.TOUCH | avg.MOUSE, None)
129
self._polyline.unlink()
131
def in_between(val,b1,b2):
132
"""return True if val is between b1 and b2"""
133
return((b1>=val and val>=b2) or (b1<=val and val<=b2))
135
def line_collide(line1,line2):
138
c=line1[1].x-line1[0].x
139
d=line1[1].y-line1[0].y
142
g=line2[1].x-line2[0].x
143
h=line2[1].y-line2[0].y
146
if dem==0: # parallel
149
s=(a*d+f*c-b*c-e*d)/dem
154
def line_intersect(line1, line2):
162
p = line_collide(line1, line2)
163
if(p and in_between(p.x,ka.x,kb.x) # do line segments match?
164
and in_between(p.x,la.x,lb.x)
165
and in_between(p.y,ka.y,kb.y)
166
and in_between(p.y,la.y,lb.y)):
173
def __init__(self, gameController, pos, edge1, edge2):
174
self.__edges = edge1, edge2
175
self.__gameController = gameController
176
gameController.level.addClash() #XXX
177
edge1.addClash(edge2, self)
178
edge2.addClash(edge1, self)
179
#self.__node = g_player.createNode('image',{
180
# 'href':'clash.png',
183
self.__node = g_player.createNode('circle',{
188
self.__node = g_player.createNode('rect', {
189
'size':Point2D(20,20)*g_scale,
190
'strokewidth':3*g_scale,
192
gameController.clashDiv.appendChild(self.__node)
196
self.__node.pos = pos - self.__node.size/2
199
edge1, edge2 = self.__edges
200
edge1.removeClash(edge2)
201
edge2.removeClash(edge1)
204
self.__gameController.level.removeClash() #XXX
208
def __init__(self, gameController, vertex1, vertex2):
209
self.__vertices = vertex1, vertex2
210
for vertex in self.__vertices:
213
self.__gameController = gameController
215
self.__line = g_player.createNode('line', {'strokewidth':3*g_scale})
216
gameController.edgeDiv.appendChild(self.__line)
218
self.__clashState = False
221
return [v.pos for v in self.__vertices]
223
def checkCollisions(self):
224
for other in self.__gameController.getEdges():
225
pos = line_intersect(self.getLine(), other.getLine())
226
if other in self.__clashes.keys():
228
self.__clashes[other].goto(pos)
230
self.__clashes[other].delete()
232
elif pos: # new clash
233
Clash(self.__gameController, pos, self, other)
236
def onVertexMotion(self):
237
clashRemoved = self.checkCollisions()
241
def addClash(self, other, clash):
242
assert other not in self.__clashes.keys()
243
self.__clashes[other] = clash
244
self.updateClashState()
246
def removeClash(self, other):
247
del self.__clashes[other]
248
self.updateClashState()
251
self.__line.pos1 = self.__vertices[0].pos
252
self.__line.pos2 = self.__vertices[1].pos
254
self.__line.color = 'ff6000' # red
256
self.__line.color = 'ffffff' # white
258
def updateClashState(self):
259
clashState = self.isClashed()
260
if clashState != self.__clashState:
261
self.__clashState = clashState
263
for vertex in self.__vertices:
264
vertex.updateClashState()
267
return len(self.__clashes) > 0
270
for clash in self.__clashes.values():
277
class Vertex(object):
278
def __init__(self, gameController, pos):
279
self._gameController = gameController
281
self.__node = g_player.createNode('image', {'href':'vertex.png'})
282
parent = gameController.vertexDiv
283
parent.appendChild(self.__node)
284
self.__node.size *= g_scale
285
self.__nodeOffset = self.__node.size / 2
286
self.__node.pos = pos - self.__nodeOffset
287
self.__clashState = False
288
self._highlight = False
289
self.draggable = True
292
if not self.draggable:
294
delta = getDelta(event.motion, self.__node.pos,
295
self.__node.pos + self.__node.size, parent.size)
298
self.__button = MoveButton(self.__node, onMotion=onMotion)
300
def addEdge(self, edge):
301
self.__edges.append(edge)
303
def updateClashState(self):
305
for edge in self.__edges:
310
if clashState != self.__clashState:
311
self.__clashState = clashState
312
self.__setNodeImage()
314
def highlight(self, addHighlighter):
315
self._highlight = addHighlighter
316
self.__setNodeImage()
318
def __setNodeImage(self):
323
if self.__clashState:
324
self.__node.href = href + '_clash.png'
326
self.__node.href = href + '.png'
330
return self.__node.pos + self.__nodeOffset
333
def pos(self, value):
334
self.__node.pos = value - self.__nodeOffset
336
for edge in self.__edges:
337
clashRemoved |= edge.onVertexMotion()
339
self._gameController.level.checkWin()
343
return self.__node.size
346
self.__button.delete()
354
def __init__(self, gameController):
355
self.__gameController = gameController
356
self.__isRunning = False
357
self.__numClashes = 0
358
self._vertexGroups = []
361
self.__numClashes +=1
362
self.__gameController.updateStatus()
364
def removeClash(self):
365
assert self.__numClashes > 0
366
self.__numClashes -=1
367
self.__gameController.updateStatus()
370
type_, number = self.__scoring[2:4]
371
return "clashes left: %u<br/>goal %c %u" %(self.__numClashes, type_, number)
374
return self.__levelData['name']
378
type_, number = self.__scoring[2:4]
379
if ((type_=='=' and self.__numClashes == number)
380
or (type_=='<' and self.__numClashes < number)
381
or (self.__numClashes <= number)):
382
self.__gameController.levelWon()
384
def start(self, levelData):
385
self.__levelData = levelData
386
self.__scoring = levelData["scoring"]
387
self.__levelData['menuItem'].color = 'ffffff' # unlock level -> white
389
for vertexCoord in levelData["vertices"]:
390
self.vertices.append(Vertex(self.__gameController, vertexCoord))
393
for v1, v2 in levelData["edges"]:
394
self.edges.append(Edge(self.__gameController, self.vertices[v1], self.vertices[v2]))
396
for edge in self.edges:
397
edge.checkCollisions()
399
self.__isRunning = True
402
self.__isRunning = False
405
self.__isRunning = False
406
for edge in self.edges:
409
for group in self._vertexGroups:
411
self._vertexGroups = []
413
for vertex in self.vertices:
417
def getEnclosedVertices(self, polygon):
418
return [vertex for vertex in self.vertices
419
if avg.pointInPolygon(vertex.pos, polygon)]
421
def addVertexGroup(self, group):
422
self._vertexGroups.append(group)
425
def loadLevels(size):
426
fp = gzip.open(getMediaDir(__file__, 'data/levels.pickle.gz'))
427
levels = cPickle.load(fp)
431
vertices = level['vertices']
432
minPos = Point2D(size)
433
maxPos = Point2D(0, 0)
434
for i in xrange(len(vertices)):
435
vertices[i] = Point2D(vertices[i][0]*g_scale, vertices[i][1]*g_scale)
436
if vertices[i].x < minPos.x:
437
minPos.x = vertices[i].x
438
if vertices[i].y < minPos.y:
439
minPos.y = vertices[i].y
440
if vertices[i].x > maxPos.x:
441
maxPos.x = vertices[i].x
442
if vertices[i].y > maxPos.y:
443
maxPos.y = vertices[i].y
444
# center level on screen
445
levelSize = maxPos - minPos
446
levelOffset = (size - levelSize) / 2 - minPos
447
for i in xrange(len(vertices)):
448
vertices[i] += levelOffset
453
class GameController(object):
454
def __init__(self, parentNode, onExit):
455
self.node = parentNode
456
self.__levels = loadLevels(parentNode.size)
458
background = g_player.createNode('image', {'href':'black.png'})
459
background.size = parentNode.size
460
parentNode.appendChild(background)
462
self.gameDiv = g_player.createNode('div', {})
463
parentNode.appendChild(self.gameDiv)
465
self.edgeDiv = g_player.createNode('div', {'sensitive':False})
466
self.groupDiv = g_player.createNode('div', {'sensitive':False})
467
self.vertexDiv = g_player.createNode('div', {})
468
self.vertexDiv.setEventHandler(avg.CURSORDOWN, avg.TOUCH | avg.MOUSE,
470
self.clashDiv = g_player.createNode('div', {'sensitive':False})
472
self._groupedVertices = set()
474
for div in (self.edgeDiv, self.vertexDiv, self.clashDiv, self.groupDiv):
475
self.gameDiv.appendChild(div)
476
div.size = parentNode.size
478
self.winnerDiv = g_player.createNode('words', {
480
'fontsize':100*g_scale,
483
parentNode.appendChild(self.winnerDiv)
484
self.winnerDiv.pos = (parentNode.size - self.winnerDiv.getMediaSize()) / 2
486
pos = Point2D(50, 50)
488
LabelButton(parentNode, 'exit', 30*g_scale, onExit, pos*g_scale)
490
LabelButton(parentNode, 'levels', 30*g_scale,
491
lambda:self.levelMenu.open(self.__curLevel-1), pos*g_scale)
493
statusNode = g_player.createNode('words', {
494
'pos':(parentNode.width-50*g_scale, 50*g_scale),
495
'fontsize':30*g_scale,
498
parentNode.appendChild(statusNode)
501
statusNode.text = text
502
self.__statusHandler = setStatus
504
levelNameDiv = g_player.createNode('div', {'sensitive':False})
505
self.gameDiv.appendChild(levelNameDiv)
506
bgImage = g_player.createNode('image', {'href':'menubg.png'})
507
levelNameDiv.appendChild(bgImage)
508
levelNameNode = g_player.createNode('words', {
509
'fontsize':30*g_scale,
510
'pos':Point2D(20, 20)*g_scale,
512
levelNameDiv.appendChild(levelNameNode)
514
def setLevelName(text):
515
levelNameNode.text = text
516
levelNameSize = levelNameNode.getMediaSize()
517
bgImage.size = levelNameSize + Point2D(40, 40) * g_scale
518
levelNameDiv.pos = parentNode.size / 2 - bgImage.size / 2
519
levelNameDiv.opacity = 1
520
avg.fadeOut(levelNameDiv, 6000)
521
self.__levelNameHandler = setLevelName
523
self.levelMenu = LevelMenu(parentNode, self.__levels, self.switchLevel)
526
self.level = Level(self)
527
self.__startNextLevel()
530
return self.level.edges
532
def updateStatus(self):
533
self.__statusHandler(self.level.getStatus())
535
def switchLevel(self, levelIndex):
536
self.__curLevel = levelIndex
539
def __startNextLevel(self):
540
self.__curLevel %= len(self.__levels)
541
level = self.__levels[self.__curLevel]
542
self.level.start(level)
543
self.__levelNameHandler(self.level.getName())
546
def levelWon(self, showWinnerDiv=True):
549
self.__startNextLevel()
551
avg.fadeOut(self.winnerDiv, 400)
552
avg.fadeIn(self.gameDiv, 400)
555
avg.fadeIn(self.winnerDiv, 600)
556
avg.fadeOut(self.gameDiv, 600, lambda: g_player.setTimeout(1000, nextLevel))
558
avg.fadeOut(self.gameDiv, 600, nextLevel)
560
def groupVertices(self, polygon):
561
vertices = set(self.level.getEnclosedVertices(polygon))
562
newGroup = vertices - self._groupedVertices
563
self._groupedVertices = self._groupedVertices.union(newGroup)
564
for vertex in newGroup:
565
vertex.highlight(True)
566
vertex.draggable = False
567
return list(newGroup)
569
def ungroupVertices(self, vertices):
570
for vertex in vertices:
571
vertex.highlight(False)
572
vertex.draggable = True
573
self._groupedVertices -= set(vertices)
575
def _onDraw(self, event):
576
GroupDetector(self, event)
582
def __init__(self, parentNode, levels, callback):
583
# main div catches all clicks and disables game underneath
584
mainDiv = g_player.createNode('div', {
585
'size':parentNode.size,
588
parentNode.appendChild(mainDiv)
590
fontSize = round(16 * g_scale)
591
itemHeight = fontSize * 3
592
listHeight = itemHeight * self.VISIBLE_LEVELS
594
menuSize = Point2D(round(mainDiv.width*0.75), listHeight+itemHeight)
595
menuDiv = g_player.createNode('div', {
596
'pos':(mainDiv.size-menuSize)/2,
598
mainDiv.appendChild(menuDiv)
600
bgImage = g_player.createNode('image', {
602
'size':menuDiv.size})
603
menuDiv.appendChild(bgImage)
605
listFrameDiv = g_player.createNode('div', {
606
'size':(menuDiv.width, listHeight),
608
menuDiv.appendChild(listFrameDiv)
610
selectionBg = g_player.createNode('rect', {
611
'pos':(-1, (listFrameDiv.height-itemHeight)/2),
612
'size':(listFrameDiv.width+2, itemHeight),
613
'fillcolor':'ff6000'}) # red
614
listFrameDiv.appendChild(selectionBg)
616
listDiv = g_player.createNode('div', {
618
listFrameDiv.appendChild(listDiv)
620
pos = Point2D(listFrameDiv.width/2, 0)
622
menuItem = g_player.createNode('words', {
623
'text':level['name'],
625
'color':'7f7f7f', # initially locked -> gray
626
'alignment':'center'})
627
menuItem.pos = pos + Point2D(0, (itemHeight-menuItem.getMediaSize().y)/2)
628
level['menuItem'] = menuItem
629
listDiv.appendChild(menuItem)
632
separatorLine = g_player.createNode('line', {
633
'pos1':(0, listHeight),
634
'pos2':(menuDiv.width, listHeight)})
635
menuDiv.appendChild(separatorLine)
637
listDivMaxPos = selectionBg.pos.y
638
listDivMinPos = -pos.y + listDivMaxPos + itemHeight
640
def onOpen(levelIndex):
641
mainDiv.active = True
642
self.__selectedLevelIndex = levelIndex
643
listDiv.pos = (0, listDivMaxPos - levelIndex * itemHeight)
644
selectionBg.fillopacity = 0.5
645
self.__motionDiff = 0
646
self.__lastTargetPos = listDiv.pos.y
647
avg.fadeIn(mainDiv, 400)
648
self.__onOpenHandler = onOpen
652
mainDiv.active = False
653
avg.fadeOut(mainDiv, 400, setInactive)
656
callback(self.__selectedLevelIndex)
660
self.__motionDiff = 0
663
self.__motionDiff += event.motion.y
664
motion = round(self.__motionDiff / itemHeight) * itemHeight
666
pos = (0, min(max(self.__lastTargetPos+motion, listDivMinPos), listDivMaxPos))
667
avg.LinearAnim(listDiv, 'pos', 200, listDiv.pos, pos).start()
668
self.__motionDiff -= motion
669
self.__lastTargetPos = pos[1]
670
self.__selectedLevelIndex = int((listDivMaxPos-self.__lastTargetPos) / itemHeight)
671
if levels[self.__selectedLevelIndex]['menuItem'].color == 'ffffff':
672
startBtn.setActive(True)
673
avg.LinearAnim(selectionBg, 'fillopacity', 200,
674
selectionBg.fillopacity, 0.5).start()
676
startBtn.setActive(False)
677
avg.LinearAnim(selectionBg, 'fillopacity', 200,
678
selectionBg.fillopacity, 0).start()
680
MoveButton(listFrameDiv, onUpDown, onUpDown, onMotion)
681
startBtn = LabelButton(menuDiv, 'start level', 20*g_scale, onStart)
682
startBtn.setPos((itemHeight*2, listHeight+(itemHeight-startBtn.size.y)/2))
683
closeBtn = LabelButton(menuDiv, 'close menu', 20*g_scale, onClose)
684
closeBtn.setPos((menuDiv.width-itemHeight*2-closeBtn.size.x,
685
listHeight+(itemHeight-closeBtn.size.y)/2))
687
def open(self, levelIndex):
688
self.__onOpenHandler(levelIndex)
691
class Planarity(AVGApp):
694
self._parentNode.mediadir = getMediaDir(__file__)
697
size = self._parentNode.size
698
g_scale = min(size.x / BASE_SIZE.x, size.y / BASE_SIZE.y)
700
self.__controller = GameController(self._parentNode, onExit = self.leave)
703
#self.__controller.startLevel()
710
if __name__ == '__main__':
712
Planarity.start(resolution = BASE_SIZE)