File indexing completed on 2024-04-21 04:01:51

0001 # -*- coding: utf-8 -*-
0002 
0003 """Copyright (C) 2009-2016 Wolfgang Rohdewald <wolfgang@rohdewald.de>
0004 
0005 SPDX-License-Identifier: GPL-2.0
0006 
0007 Read the user manual for a description of the interface to this scoring engine
0008 """
0009 
0010 
0011 from itertools import chain
0012 
0013 from mi18n import i18nc
0014 from common import StrMixin
0015 from tile import Tile, TileList, elements
0016 
0017 
0018 class Meld(TileList, StrMixin):
0019 
0020     """represents a meld. Can be empty. Many Meld methods will
0021     raise exceptions if the meld is empty. But we do not care,
0022     those methods are not supposed to be called on empty melds.
0023     Meld is essentially a list of Tile with added methods.
0024     A Meld is immutable, not from the view of python but for
0025     its user
0026 
0027     for melds with 3 tiles::
0028         isDeclared == isExposed : 3 exposed tiles
0029         not isDeclared == isConcealed: 3 concealed Tiles
0030         exposed = aaa
0031         exposedClaimed = aaa
0032 
0033     for melds with 4 tiles::
0034         isKong = aAAa or aaaa or aaaA but NOT AAAA
0035         isDeclared = aAAa or aaaa or aaaA
0036         isExposed = aaaa or aaaA
0037         isConcealed = aAAa or AAAA
0038         exposedClaimed = aaaA
0039         exposed = aaaa
0040 
0041     """
0042     # pylint: disable=too-many-instance-attributes
0043 
0044     __hash__ = None
0045     cache = {}
0046 
0047     def __new__(cls, newContent=None):
0048         """try to use cache"""
0049         if isinstance(newContent, str) and newContent in cls.cache:
0050             return cls.cache[newContent]
0051         if isinstance(newContent, Meld):
0052             return newContent
0053         tiles = TileList(newContent)
0054         cacheKey = tiles.key()
0055         if cacheKey in cls.cache:
0056             return cls.cache[cacheKey]
0057         return TileList.__new__(cls, tiles)
0058 
0059     @classmethod
0060     def check(cls):
0061         """check cache consistency"""
0062         for key, value in cls.cache.items():
0063             assert key == value.key or key == str(value), 'cache wrong: cachekey=%s realkey=%s value=%s' % (
0064                 key, value.key, value)
0065             assert value.key == 1 + value.hashTable.index(value) / 2
0066             assert value.key == TileList.key(value), \
0067                 'static key:%s current key:%s, static value:%s, current value:%s ' % (
0068                     value.key, TileList.key(value), value.original, value)
0069 
0070     def __init__(self, newContent=None):
0071         """init the meld: content can be either
0072         - a single string with 2 chars for every tile
0073         - a list containing such strings
0074         - another meld. Its tiles are not passed.
0075         - a list of Tile objects"""
0076         if not hasattr(self, '_fixed'):  # already defined if I am from cache
0077             TileList.__init__(self, newContent)
0078             self.case = ''.join('a' if x.islower() else 'A' for x in self)
0079             self.key = TileList.key(self)
0080             if self.key not in self.cache:
0081                 self.cache[self.key] = self
0082                 self.cache[str(self)] = self
0083             self.isExposed = self.__isExposed()
0084             self.isConcealed = not self.isExposed
0085             self.isSingle = self.isPair = self.isChow = self.isPung = False
0086             self.isKong = self.isClaimedKong = self.isKnitted = False
0087             self.isDragonMeld = len(self) and self[0].isDragon
0088             self.isWindMeld = len(self) and self[0].isWind
0089             self.isHonorMeld = self.isDragonMeld or self.isWindMeld
0090             self.isBonus = len(self) == 1 and self[0].isBonus
0091             self.isKnown = len(self) and self[0].isKnown
0092             self.__setMeldType()
0093             self.isPungKong = self.isPung or self.isKong
0094             self.isDeclared = self.isExposed or self.isKong
0095             groups = {x.group.lower() for x in self}
0096             if len(groups) == 1:
0097                 self.group = self[0].group
0098                 self.lowerGroup = self.group.lower()
0099             else:
0100                 self.group = 'X'
0101                 self.lowerGroup = 'x'
0102             self.isRest = False
0103             self.__staticRules = {}  # ruleset is key
0104             self.__dynamicRules = {}  # ruleset is key
0105             self.__staticDoublingRules = {}  # ruleset is key
0106             self.__dynamicDoublingRules = {}  # ruleset is key
0107             self.__hasRules = None  # unknown yet
0108             self.__hasDoublingRules = None  # unknown yet
0109             self.concealed = self.exposed = self.declared = self.exposedClaimed = None  # to satisfy pylint
0110             self._fixed = True
0111 
0112             if len(self) < 4:
0113                 TileList.__setattr__(
0114                     self,
0115                     'concealed',
0116                     Meld(TileList(x.concealed for x in self)))
0117                 TileList.__setattr__(self, 'declared', self.concealed)
0118                 TileList.__setattr__(
0119                     self,
0120                     'exposed',
0121                     Meld(TileList(x.exposed for x in self)))
0122                 TileList.__setattr__(self, 'exposedClaimed', self.exposed)
0123             else:
0124                 TileList.__setattr__(
0125                     self,
0126                     'concealed',
0127                     Meld(TileList(x.concealed for x in self)))
0128                 TileList.__setattr__(
0129                     self, 'declared',
0130                     Meld(TileList([self[0].exposed, self[1].concealed, self[2].concealed, self[3].exposed])))
0131                 TileList.__setattr__(
0132                     self,
0133                     'exposed',
0134                     Meld(TileList(x.exposed for x in self)))
0135                 TileList.__setattr__(
0136                     self, 'exposedClaimed',
0137                     Meld(TileList([self[0].exposed, self[1].exposed, self[2].exposed, self[3].concealed])))
0138 
0139     def __setattr__(self, name, value):
0140         if (hasattr(self, '_fixed')
0141                 and not name.endswith('__hasRules')
0142                 and not name.endswith('__hasDoublingRules')):
0143             raise TypeError
0144         TileList.__setattr__(self, name, value)
0145 
0146     def __prepareRules(self, ruleset):
0147         """prepare rules from ruleset"""
0148         rulesetId = id(ruleset)
0149         self.__staticRules[rulesetId] = [
0150             x for x in ruleset.meldRules
0151             if not hasattr(x, 'mayApplyToMeld') and x.appliesToMeld(None, self)]
0152         self.__dynamicRules[rulesetId] = [
0153             x for x in ruleset.meldRules
0154             if hasattr(x, 'mayApplyToMeld') and x.mayApplyToMeld(self)]
0155         self.__hasRules = any(len(x) for x in chain(
0156             self.__staticRules.values(), self.__dynamicRules.values()))
0157 
0158         self.__staticDoublingRules[rulesetId] = [
0159             x for x in ruleset.doublingMeldRules
0160             if not hasattr(x, 'mayApplyToMeld') and x.appliesToMeld(None, self)]
0161         self.__dynamicDoublingRules[rulesetId] = [x for x in ruleset.doublingMeldRules
0162                                                   if hasattr(x, 'mayApplyToMeld') and x.mayApplyToMeld(self)]
0163         self.__hasDoublingRules = any(len(x) for x in chain(
0164             self.__staticDoublingRules.values(), self.__dynamicDoublingRules.values()))
0165 
0166     def rules(self, hand):
0167         """all applicable rules for this meld being part of hand"""
0168         if self.__hasRules is False:
0169             return []
0170         ruleset = hand.ruleset
0171         rulesetId = id(ruleset)
0172         if rulesetId not in self.__staticRules:
0173             self.__prepareRules(ruleset)
0174         result = self.__staticRules[rulesetId][:]
0175         result.extend(x for x in self.__dynamicRules[
0176             rulesetId] if x.appliesToMeld(hand, self))
0177         return result
0178 
0179     def doublingRules(self, hand):
0180         """all applicable doubling rules for this meld being part of hand"""
0181         ruleset = hand.ruleset
0182         rulesetId = id(ruleset)
0183         if rulesetId not in self.__staticRules:
0184             self.__prepareRules(ruleset)
0185         result = self.__staticDoublingRules[rulesetId][:]
0186         result.extend(x for x in self.__dynamicDoublingRules[
0187             rulesetId] if x.appliesToMeld(hand, self))
0188         return result
0189 
0190     def append(self, unused):
0191         """we want to be immutable"""
0192         raise TypeError
0193 
0194     def extend(self, unused):
0195         """we want to be immutable"""
0196         raise TypeError
0197 
0198     def insert(self, unused):
0199         """we want to be immutable"""
0200         raise TypeError
0201 
0202     def pop(self, unused):
0203         """we want to be immutable"""
0204         raise TypeError
0205 
0206     def remove(self, unused):
0207         """we want to be immutable"""
0208         raise TypeError
0209 
0210     def without(self, remove):
0211         """self without tile. The rest will be uppercased."""
0212         tiles = TileList()
0213         for tile in self:
0214             if tile is remove:
0215                 remove = None
0216             else:
0217                 tiles.append(tile.concealed)
0218         return tiles
0219 
0220     def __setitem__(self, index, value):
0221         """set a tile in the meld"""
0222         raise TypeError
0223 
0224     def __delitem__(self, index):
0225         """removes a tile from the meld"""
0226         raise TypeError
0227 
0228     def __isExposed(self):
0229         """meld state: exposed or not"""
0230         if self.case.islower():
0231             return True
0232         if len(self) == 4:
0233             return self.case[1:3].islower()
0234         return False
0235 
0236     def __setMeldType(self):
0237         """compute meld type. Except knitting melds."""
0238         # pylint: disable=too-many-branches,too-many-return-statements
0239         length = len(self)
0240         if length == 0:
0241             return
0242         if length > 4:
0243             raise UserWarning('Meld %s is too long' % self)
0244         if any(not x.isKnown for x in self):
0245             if len(set(self)) != 1:
0246                 raise UserWarning(
0247                     'Meld %s: Cannot mix known and unknown tiles')
0248             self.isKnown = False
0249             return
0250         if length == 1:
0251             self.isSingle = True
0252             return
0253         if length == 2:
0254             if self[0] == self[1]:
0255                 self.isPair = True
0256             elif self[0].value == self[1].value and self.case[0] == self.case[1] \
0257                     and all(x.lowerGroup in Tile.colors for x in self):
0258                 self.isKnitted = True
0259             else:
0260                 raise UserWarning('Meld %s is malformed' % self)
0261             return
0262         # now length is 3 or 4
0263         tiles = set(self)
0264         if len(tiles) == 1:
0265             if length == 3:
0266                 self.isPung = True
0267             elif self.case != 'AAAA':
0268                 self.isKong = True
0269             return
0270         if len(tiles) == 3 and length == 3:
0271             if len({x.value for x in tiles}) == 1:
0272                 if self.case in ('aaa', 'AAA'):
0273                     if len({x.group for x in tiles}) == 3:
0274                         if all(x.lowerGroup in Tile.colors for x in tiles):
0275                             self.isKnitted = True
0276                             return
0277         groups = {x.group for x in self}
0278         if len(groups) > 2 or len({x.lower() for x in groups}) > 1:
0279             raise UserWarning('Meld %s is malformed' % self)
0280         values = {x.value for x in self}
0281         if length == 4:
0282             if len(values) > 1:
0283                 raise UserWarning('Meld %s is malformed' % self)
0284             if self.case == 'aaaA':
0285                 self.isKong = self.isClaimedKong = True
0286             elif self.case == 'aAAa':
0287                 self.isKong = True
0288             else:
0289                 raise UserWarning('Meld %s is malformed' % self)
0290             return
0291         # only possibilities left are CHOW and REST
0292         # length is 3
0293         if len(groups) == 1:
0294             if groups.pop().lower() in Tile.colors:
0295                 if self[0].nextForChow is self[1] and self[1].nextForChow is self[2]:
0296                     self.isChow = True
0297                     return
0298         raise UserWarning('Meld %s is malformed' % self)
0299 
0300     def __lt__(self, other):
0301         """used for sorting. Smaller value is shown first."""
0302         if not other:
0303             return False
0304         if not self:
0305             return True
0306         if self.isDeclared and not other.isDeclared:
0307             return True
0308         if not self.isDeclared and other.isDeclared:
0309             return False
0310         if self[0].key == other[0].key:
0311             return len(self) > len(other)
0312         return self[0].key < other[0].key
0313 
0314     def typeName(self):
0315         """convert int to speaking name with shortcut. ATTENTION: UNTRANSLATED!"""
0316         # pylint: disable=too-many-return-statements
0317         if self.isBonus:
0318             return i18nc('kajongg meld type', 'Bonus')
0319         if self.isSingle:
0320             return i18nc('kajongg meld type', '&single')
0321         if self.isPair:
0322             return i18nc('kajongg meld type', '&pair')
0323         if self.isChow:
0324             return i18nc('kajongg meld type', '&chow')
0325         if self.isPung:
0326             return i18nc('kajongg meld type', 'p&ung')
0327         if self.isClaimedKong:
0328             return i18nc('kajongg meld type', 'c&laimed kong')
0329         if self.isKong:
0330             return i18nc('kajongg meld type', 'k&ong')
0331         return i18nc('kajongg meld type', 'rest of tiles')
0332 
0333     def __stateName(self):
0334         """the translated name of the state"""
0335         if self.isBonus or self.isClaimedKong:
0336             return ''
0337         if self.isExposed:
0338             return i18nc('kajongg meld state', 'Exposed')
0339         return i18nc('kajongg meld state', 'Concealed')
0340 
0341     def name(self):
0342         """the long name"""
0343         result = i18nc(
0344             'kajongg meld name, do not translate parameter names',
0345             '{state} {meldType} {name}')
0346         return result.format(
0347             state=self.__stateName(),
0348             meldType=self.typeName(),
0349             name=self[0].name()).replace('  ', ' ').strip()
0350 
0351     @staticmethod
0352     def cacheMeldsInTiles():
0353         """define all usual melds as Tile attributes"""
0354         Tile.unknown.single = Meld(Tile.unknown)
0355         Tile.unknown.pung = Meld(Tile.unknown * 3)
0356         for tile, occ in elements.occurrence.items():
0357             tile.single = Meld(tile)
0358             tile.concealed.single = Meld(tile.concealed)
0359             if occ > 1:
0360                 tile.pair = Meld(tile * 2)
0361                 tile.concealed.pair = Meld(tile.concealed * 2)
0362                 if occ > 2:
0363                     tile.pung = Meld(tile * 3)
0364                     tile.concealed.pung = Meld(tile.concealed * 3)
0365                     if tile.value in range(1, 8):
0366                         tile.chow = Meld(
0367                             [tile,
0368                              tile.nextForChow,
0369                              tile.nextForChow.nextForChow])
0370                         tile.concealed.chow = Meld(
0371                             [tile.concealed, tile.nextForChow.concealed,
0372                              tile.nextForChow.nextForChow.concealed])
0373                     if tile.value in range(1, 10):
0374                         tile.knitted3 = Meld(
0375                             [Tile(x, tile.value) for x in Tile.colors])
0376                         tile.concealed.knitted3 = Meld(
0377                             [Tile(x, tile.value).concealed for x in Tile.colors])
0378                     if occ > 3:
0379                         tile.kong = Meld(tile * 4)
0380                         tile.claimedKong = Meld(
0381                             [tile, tile, tile, tile.concealed])
0382                         tile.concealed.kong = Meld(tile.concealed * 4)
0383 
0384 
0385 class MeldList(list):
0386 
0387     """a list of melds"""
0388 
0389     def __init__(self, newContent=None):
0390         list.__init__(self)
0391         if newContent is None:
0392             return
0393         if isinstance(newContent, Meld):
0394             list.append(self, newContent)
0395         elif isinstance(newContent, str):
0396             list.extend(self, [Meld(x)
0397                                for x in newContent.split()])  # pylint: disable=maybe-no-member
0398         else:
0399             list.extend(self, [Meld(x) for x in newContent])
0400         self.sort()
0401 
0402     def extend(self, values):
0403         list.extend(self, values)
0404         self.sort()
0405 
0406     def append(self, value):
0407         list.append(self, value)
0408         self.sort()
0409 
0410     def tiles(self):
0411         """flat view of all tiles in all melds"""
0412         return TileList(sum(self, []))
0413 
0414     def __str__(self):
0415         if self:
0416             return ' '.join(str(x) for x in self)
0417         return ''
0418 
0419 Meld.cacheMeldsInTiles()