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