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()