File indexing completed on 2024-04-21 04:01:56
0001 # -*- coding: utf-8 -*- 0002 0003 """ 0004 Copyright (C) 2008-2016 Wolfgang Rohdewald <wolfgang@rohdewald.de> 0005 0006 SPDX-License-Identifier: GPL-2.0 0007 0008 """ 0009 0010 from common import Internal, ZValues, StrMixin, Speeds, DrawOnTopMixin 0011 from wind import Wind, East 0012 from qt import QPointF, QGraphicsObject, QFontMetrics 0013 from qt import QPen, QColor, QFont, Qt, QRectF 0014 0015 from guiutil import Painter, sceneRotation 0016 from board import PlayerWind, YellowText, Board 0017 from wall import Wall, KongBox 0018 from tile import Tile 0019 from tileset import Tileset 0020 from uitile import UITile 0021 from animation import animate, afterQueuedAnimations, AnimationSpeed 0022 from animation import ParallelAnimationGroup, AnimatedMixin, animateAndDo 0023 0024 0025 class SideText(AnimatedMixin, QGraphicsObject, StrMixin, DrawOnTopMixin): 0026 0027 """The text written on the wall""" 0028 0029 sideTexts = list() 0030 0031 def __init__(self, parent=None): 0032 assert parent is None 0033 assert len(self.sideTexts) < 4 0034 self.__name = 't%d' % len(self.sideTexts) 0035 self.sideTexts.append(self) 0036 super().__init__() 0037 self.hide() 0038 Internal.scene.addItem(self) 0039 self.__text = '' 0040 self.__board = None 0041 self.needsRefresh = False 0042 self.__color = Qt.black 0043 self.__boundingRect = None 0044 self.__font = None 0045 0046 def adaptedFont(self): 0047 """Font with the correct point size for the wall""" 0048 result = QFont() 0049 size = 80 0050 result.setPointSize(size) 0051 tileHeight = self.board.tileset.faceSize.height() 0052 while QFontMetrics(result).ascent() > tileHeight: 0053 size -= 1 0054 result.setPointSize(size) 0055 return result 0056 0057 @staticmethod 0058 def refreshAll(): 0059 """recompute ourself. Always do this for all for sides 0060 together because if two names change place we want the 0061 to move simultaneously""" 0062 sides = SideText.sideTexts 0063 if all(not x.needsRefresh for x in sides): 0064 return 0065 rotating = False 0066 for side in sides: 0067 side.show() 0068 if not side.needsRefresh: 0069 continue 0070 side.needsRefresh = False 0071 rotating |= sceneRotation(side) != sceneRotation(side.board) 0072 0073 alreadyMoved = any(x.x() for x in sides) 0074 with AnimationSpeed(speed=Speeds.windMarker if rotating and alreadyMoved else 99): 0075 # first round: just place the winds. Only animate moving them 0076 # for later rounds. 0077 for side in sides: 0078 side.setupAnimations() 0079 animate() 0080 0081 @staticmethod 0082 def removeAll(): 0083 """from the scene""" 0084 for side in SideText.sideTexts: 0085 Internal.scene.removeItem(side) 0086 SideText.sideTexts = list() 0087 0088 def moveDict(self): 0089 """return a dict with new property values for our sidetext 0090 which move it onto us""" 0091 if not self.board or not self.__text: 0092 return {} 0093 rotation = sceneRotation(self.board) 0094 position = self.board.center() 0095 textCenter = self.boundingRect().center() 0096 if rotation == 180: 0097 rotation = 0 0098 position += textCenter 0099 else: 0100 position -= textCenter 0101 return {'pos': self.board.mapToScene(position), 'rotation': rotation, 'scale': self.board.scale()} 0102 0103 def name(self): 0104 """for identification in animations""" 0105 return self.__name 0106 0107 @property 0108 def board(self): 0109 """the front we are sitting on""" 0110 return self.__board 0111 0112 @board.setter 0113 def board(self, value): 0114 if self.__board != value: 0115 self.__board = value 0116 self.__font = self.adaptedFont() 0117 self.needsRefresh = True 0118 0119 @property 0120 def color(self): 0121 """text color""" 0122 return self.__color 0123 0124 @color.setter 0125 def color(self, value): 0126 if self.__color != value: 0127 self.__color = value 0128 self.update() 0129 0130 @property 0131 def text(self): 0132 """what we are saying""" 0133 return self.__text 0134 0135 @text.setter 0136 def text(self, value): 0137 if self.__text != value: 0138 self.__text = value 0139 self.prepareGeometryChange() 0140 txt = self.__text 0141 if ' - ' in txt: 0142 # this disables animated movement if only the score changes 0143 txt = txt[:txt.rfind(' - ')] + ' - 55' 0144 self.__boundingRect = QRectF(QFontMetrics(self.__font).boundingRect(txt)) 0145 self.needsRefresh = True 0146 0147 def paint(self, painter, unusedOption, unusedWidget=None): 0148 """paint the marker""" 0149 with Painter(painter): 0150 pen = QPen(QColor(self.color)) 0151 painter.setPen(pen) 0152 painter.setFont(self.__font) 0153 painter.drawText(0, 0, self.__text) 0154 0155 def boundingRect(self): 0156 """around the text""" 0157 return self.__boundingRect or QRectF() 0158 0159 def __str__(self): 0160 """for debugging""" 0161 return 'SideText(%s %s x/y= %.1f/%1f)' % ( 0162 self.name(), self.text, self.x(), self.y()) 0163 0164 0165 class UIWallSide(Board, StrMixin): 0166 0167 """a Board representing a wall of tiles""" 0168 penColor = 'red' 0169 0170 def __init__(self, tileset, boardRotation, length): 0171 Board.__init__(self, length, 1, tileset, boardRotation=boardRotation) 0172 self.length = length 0173 0174 @property 0175 def name(self): 0176 """name for debug messages""" 0177 return 'UIWallSide {}'.format(UIWall.sideNames[self.rotation()]) 0178 0179 def center(self): 0180 """return the center point of the wall in relation to the 0181 faces of the upper level""" 0182 faceRect = self.tileFaceRect() 0183 result = faceRect.topLeft() + self.shiftZ(1) + \ 0184 QPointF(self.length // 2 * faceRect.width(), faceRect.height() / 2) 0185 result.setX(result.x() + faceRect.height() / 2) # corner tile 0186 return result 0187 0188 def hide(self): 0189 """hide all my parts""" 0190 self.windTile.hide() 0191 Board.hide(self) 0192 0193 def __str__(self): 0194 """for debugging""" 0195 return self.name 0196 0197 0198 class UIKongBox(KongBox): 0199 0200 """Kong box with UITiles""" 0201 0202 def __init__(self): 0203 KongBox.__init__(self) 0204 0205 def fill(self, tiles): 0206 """fill the box""" 0207 for uiTile in self._tiles: 0208 uiTile.cross = False 0209 KongBox.fill(self, tiles) 0210 for uiTile in self._tiles: 0211 uiTile.cross = True 0212 0213 def pop(self, count): 0214 """get count tiles from kong box""" 0215 result = KongBox.pop(self, count) 0216 for uiTile in result: 0217 uiTile.cross = False 0218 return result 0219 0220 0221 class UIWall(Wall): 0222 0223 """represents the wall with four sides. self.wall[] indexes them 0224 counter clockwise, 0..3. 0 is bottom.""" 0225 0226 Lower, Right, Upper, Left = range(4) 0227 sideAngles = (0, 270, 180, 90) 0228 sideNames = {0:'Lower', 1:'Right', 2:'Upper', 3:'Left'} 0229 sideNames[270] = 'Right' 0230 sideNames[180] = 'Upper' 0231 sideNames[90] = 'Left' 0232 0233 tileClass = UITile 0234 kongBoxClass = UIKongBox 0235 0236 def __init__(self, game): 0237 """init and position the wall""" 0238 # we use only white dragons for building the wall. We could actually 0239 # use any tile because the face is never shown anyway. 0240 self.initWindMarkers() 0241 game.wall = self 0242 Wall.__init__(self, game) 0243 self.__square = Board(1, 1, Tileset.current()) 0244 self.__square.setZValue(ZValues.markerZ) 0245 sideLength = len(self.tiles) // 8 0246 self.__sides = [UIWallSide( 0247 Tileset.current(), 0248 boardRotation, sideLength) for boardRotation in self.sideAngles] 0249 for idx, side in enumerate(self.__sides): 0250 side.setParentItem(self.__square) 0251 side.lightSource = self.lightSource 0252 side.windTile = Wind.all4[idx].marker 0253 side.windTile.hide() 0254 side.message = YellowText(side) 0255 side.message.setZValue(ZValues.popupZ) 0256 side.message.setVisible(False) 0257 side.message.setPos(side.center()) 0258 self.__sides[self.Lower].setPos(yWidth=sideLength) 0259 self.__sides[self.Left].setPos(xHeight=1) 0260 self.__sides[self.Upper].setPos(xHeight=1, xWidth=sideLength, yHeight=1) 0261 self.__sides[self.Right].setPos(xWidth=sideLength, yWidth=sideLength, yHeight=1) 0262 Internal.scene.addItem(self.__square) 0263 Internal.Preferences.addWatch('showShadows', self.showShadowsChanged) 0264 0265 @staticmethod 0266 def initWindMarkers(): 0267 """the 4 round wind markers on the player walls""" 0268 if East.marker is None: 0269 for wind in Wind.all4: 0270 wind.marker = PlayerWind(wind) 0271 Internal.scene.addItem(wind.marker) 0272 0273 @staticmethod 0274 def name(): 0275 """name for debug messages""" 0276 return 'wall' 0277 0278 def __getitem__(self, index): 0279 """make Wall index-able""" 0280 return self.__sides[index] 0281 0282 def __setitem__(self, index, value): 0283 """only for pylint, currently not used""" 0284 self.__sides[index] = value 0285 0286 def __delitem__(self, index): 0287 """only for pylint, currently not used""" 0288 del self.__sides[index] 0289 0290 def __len__(self): 0291 """only for pylint, currently not used""" 0292 return len(self.__sides) 0293 0294 def hide(self): 0295 """hide all four walls and their decorators""" 0296 # may be called twice 0297 self.living = [] 0298 self.kongBox.fill([]) 0299 for side in self.__sides: 0300 side.hide() 0301 self.tiles = [] 0302 if self.__square.scene(): 0303 self.__square.scene().removeItem(self.__square) 0304 0305 def __shuffleTiles(self): 0306 """shuffle tiles for next hand""" 0307 discardBoard = Internal.scene.discardBoard 0308 places = [(x, y) for x in range(-3, discardBoard.width + 3) 0309 for y in range(-3, discardBoard.height + 3)] 0310 places = self.game.randomGenerator.sample(places, len(self.tiles)) 0311 for idx, uiTile in enumerate(self.tiles): 0312 uiTile.dark = True 0313 uiTile.setBoard(discardBoard, *places[idx]) 0314 0315 def build(self, shuffleFirst=False): 0316 """builds the wall without dividing""" 0317 # recycle used tiles 0318 for uiTile in self.tiles: 0319 uiTile.tile = Tile.unknown 0320 uiTile.dark = True 0321 # scene = Internal.scene 0322 # if not scene.game.isScoringGame() and not self.game.isFirstHand(): 0323 # speed = Internal.preferences.animationSpeed 0324 # else: 0325 speed = 99 0326 with AnimationSpeed(speed=speed): 0327 if shuffleFirst: 0328 self.__shuffleTiles() 0329 for uiTile in self.tiles: 0330 uiTile.focusable = False 0331 return animateAndDo(self.__placeWallTiles) 0332 0333 def __placeWallTiles(self, unusedResult=None): 0334 """place all wall tiles""" 0335 tileIter = iter(self.tiles) 0336 tilesPerSide = len(self.tiles) // 4 0337 for side in (self.__sides[0], self.__sides[3], 0338 self.__sides[2], self.__sides[1]): 0339 upper = True # upper tile is played first 0340 for position in range(tilesPerSide - 1, -1, -1): 0341 uiTile = next(tileIter) 0342 uiTile.setBoard(side, position // 2, 0, level=int(upper)) 0343 upper = not upper 0344 self.__setDrawingOrder() 0345 return animate() 0346 0347 @property 0348 def lightSource(self): 0349 """For possible values see LIGHTSOURCES""" 0350 return self.__square.lightSource 0351 0352 @lightSource.setter 0353 def lightSource(self, lightSource): 0354 """setting this actually changes the visuals""" 0355 if self.lightSource != lightSource: 0356 # assert ParallelAnimationGroup.current is None # may trigger, reason unknown 0357 self.__square.lightSource = lightSource 0358 for side in self.__sides: 0359 side.lightSource = lightSource 0360 self.__setDrawingOrder() 0361 SideText.refreshAll() 0362 0363 @property 0364 def tileset(self): 0365 """The tileset of this wall""" 0366 return self.__square.tileset 0367 0368 @tileset.setter 0369 def tileset(self, value): 0370 """setting this actually changes the visuals.""" 0371 if self.tileset != value: 0372 assert ParallelAnimationGroup.current is None 0373 self.__square.tileset = value 0374 self.__resizeHandBoards() 0375 SideText.refreshAll() 0376 0377 @afterQueuedAnimations 0378 def showShadowsChanged(self, deferredResult, unusedOldValue, unusedNewValue): # pylint: disable=unused-argument 0379 """setting this actually changes the visuals.""" 0380 assert ParallelAnimationGroup.current is None 0381 self.__resizeHandBoards() 0382 0383 def __resizeHandBoards(self, unusedResults=None): 0384 """we are really calling _setRect() too often. But at least it works""" 0385 for player in self.game.players: 0386 player.handBoard.computeRect() 0387 Internal.mainWindow.adjustMainView() 0388 0389 def __setDrawingOrder(self, unusedResults=None): 0390 """set drawing order of the wall""" 0391 levels = {'NW': (2, 3, 1, 0), 'NE': ( 0392 3, 1, 0, 2), 'SE': (1, 0, 2, 3), 'SW': (0, 2, 3, 1)} 0393 for idx, side in enumerate(self.__sides): 0394 side.level = ( 0395 levels[ 0396 side.lightSource][ 0397 idx] + 1) * ZValues.boardZFactor 0398 0399 def __moveDividedTile(self, uiTile, offset): 0400 """moves a uiTile from the divide hole to its new place""" 0401 board = uiTile.board 0402 newOffset = uiTile.xoffset + offset 0403 sideLength = len(self.tiles) // 8 0404 if newOffset >= sideLength: 0405 sideIdx = self.__sides.index(uiTile.board) 0406 board = self.__sides[(sideIdx + 1) % 4] 0407 uiTile.setBoard(board, newOffset % sideLength, 0, level=2) 0408 uiTile.update() 0409 0410 @afterQueuedAnimations 0411 def _placeLooseTiles(self, deferredResult=None): 0412 """place the last 2 tiles on top of kong box""" 0413 assert len(self.kongBox) % 2 == 0 0414 placeCount = len(self.kongBox) // 2 0415 if placeCount >= 4: 0416 first = min(placeCount - 1, 5) 0417 second = max(first - 2, 1) 0418 self.__moveDividedTile(self.kongBox[-1], second) 0419 self.__moveDividedTile(self.kongBox[-2], first) 0420 0421 def divide(self): 0422 """divides a wall, building a living end and a dead end""" 0423 with AnimationSpeed(): 0424 Wall.divide(self) 0425 for uiTile in self.tiles: 0426 # update graphics because tiles having been 0427 # in kongbox in a previous game 0428 # might not be there anymore. This gets rid 0429 # of the cross on them. 0430 uiTile.update() 0431 # move last two tiles onto the dead end: 0432 return self._placeLooseTiles() 0433 0434 def decorate4(self, deferredResult=None): # pylint: disable=unused-argument 0435 """show player info on the wall. The caller must ensure 0436 all are moved simultaneously and at which speed by using 0437 AnimationSpeed. 0438 already queued animations keep their speed, only the windMarkeres 0439 are moved without animation. 0440 """ 0441 with AnimationSpeed(): 0442 for player in self.game.players: 0443 player.showInfo() 0444 SideText.refreshAll() 0445 animateAndDo(self.showWindMarkers) 0446 0447 def showWindMarkers(self, unusedDeferred=None): 0448 """animate all windMarkers. The caller must ensure 0449 all are moved simultaneously and at which speed 0450 by using AnimationSpeed.""" 0451 for player in self.game.players: 0452 side = player.front 0453 side.windTile.setupAnimations() 0454 side.windTile.show()