File indexing completed on 2024-04-21 04:01:55
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 log import logException 0011 from mi18n import i18n, i18nc 0012 from common import IntDict, StrMixin 0013 from wind import Wind, East, South, West, North 0014 0015 class Tile(str, StrMixin): 0016 0017 """ 0018 A single tile, represented as a string of length 2. 0019 0020 Always True: 0021 - only for suits: tile.group + chr(tile.value + 48) == str(tile) 0022 - Tile(tile) is tile 0023 - Tile(tile.group, tile.value) is tile 0024 0025 Tile() accepts 0026 - another Tile 0027 - a string, length 2 0028 - two args: a char and either a char or an int -1..11 0029 0030 group is a char: b=bonus w=wind d=dragon X=unknown 0031 value is 0032 1..9 for real suit tiles 0033 -1/0/10/11 for usage in AI 0034 Wind for winds and boni 0035 bgr for dragons 0036 """ 0037 # pylint: disable=too-many-public-methods,too-many-instance-attributes 0038 cache = {} 0039 hashTable = 'XyxyDbdbDgdgDrdrWeweWswsWw//wwWnwn' \ 0040 'S/s/S0s0S1s1S2s2S3s3S4s4S5s5S6s6S7s7S8s8S9s9S:s:S;s;' \ 0041 'B/b/B0b0B1b1B2b2B3b3B4b4B5b5B6b6B7b7B8b8B9b9B:b:B;b;' \ 0042 'C/c/C0c0C1c1C2c2C3c3C4c4C5c5C6c6C7c7C8c8C9c9C:c:C;c;' \ 0043 'fefsfwfnyeysywyn' 0044 # the // is needed as separator between too many w's 0045 # intelligence.py will define Tile('b0') or Tile('s:') 0046 0047 unknown = None 0048 0049 # Groups: 0050 hidden = 'x' 0051 stone = 's' 0052 bamboo = 'b' 0053 character = 'c' 0054 colors = stone + bamboo + character 0055 wind = 'w' 0056 dragon = 'd' 0057 honors = wind + dragon 0058 flower = 'f' 0059 season = 'y' 0060 boni = flower + season 0061 0062 # Values: 0063 dragons = 'bgr' 0064 white, green, red = dragons 0065 winds = 'eswn' 0066 numbers = range(1, 10) 0067 terminals = list([1, 9]) 0068 minors = range(2, 9) 0069 majors = list(dragons) + list(winds) + terminals 0070 0071 def __new__(cls, *args): 0072 return cls.cache.get(args) or cls.__build(*args) 0073 0074 @classmethod 0075 def __build(cls, *args): 0076 """build a new Tile object out of args""" 0077 # pylint: disable=too-many-statements 0078 if len(args) == 1: 0079 arg0, arg1 = args[0] 0080 else: 0081 arg0, arg1 = args # pylint: disable=unbalanced-tuple-unpacking 0082 if isinstance(arg1, int): 0083 arg1 = chr(arg1 + 48) 0084 what = arg0 + arg1 0085 result = str.__new__(cls, what) 0086 result.group = result[0] 0087 result.lowerGroup = result.group.lower() 0088 result.isExposed = result.group == result.lowerGroup 0089 result.isConcealed = not result.isExposed 0090 result.isBonus = result.group in Tile.boni 0091 result.isDragon = result.lowerGroup == Tile.dragon 0092 result.isWind = result.lowerGroup == Tile.wind 0093 result.isHonor = result.isDragon or result.isWind 0094 0095 result.isTerminal = False 0096 result.isNumber = False 0097 result.isReal = True 0098 0099 if result.isWind or result.isBonus: 0100 result.value = Wind(result[1]) 0101 result.char = result[1] 0102 elif result.isDragon: 0103 result.value = result[1] 0104 result.char = result.value 0105 else: 0106 result.value = ord(result[1]) - 48 0107 result.char = result.value 0108 result.isTerminal = result.value in Tile.terminals 0109 result.isNumber = True 0110 result.isReal = result.value in Tile.numbers 0111 result.isMajor = result.isHonor or result.isTerminal 0112 result.isMinor = not result.isMajor 0113 try: 0114 result.key = 1 + result.hashTable.index(result) // 2 0115 except ValueError: 0116 logException('%s is not a valid tile string' % result) 0117 result.isKnown = Tile.unknown is not None and result != Tile.unknown 0118 for key in ( 0119 result, (str(result),), (result.group, result.value), 0120 (result[0], result[1])): 0121 cls.cache[key] = result 0122 0123 existing = list([x for x in cls.cache.values() if x.key == result.key]) # pylint: disable=consider-using-generator 0124 existingIds = {id(x) for x in existing} 0125 assert len(existingIds) == 1, 'new is:{} existing are: {} with ids {}'.format(result, existing, existingIds) 0126 0127 result.exposed = result.concealed = result.swapped = None 0128 result.single = result.pair = result.pung = None 0129 result.chow = result.kong = None 0130 result._fixed = True # pylint: disable=protected-access 0131 0132 str.__setattr__( 0133 result, 0134 'exposed', 0135 result if not result.isKnown else Tile(str.lower(result))) 0136 str.__setattr__(result, 'concealed', 0137 result if not result.isKnown or result.isBonus 0138 else Tile(str.capitalize(result))) 0139 str.__setattr__( 0140 result, 0141 'swapped', 0142 result.exposed if result.isConcealed else result.concealed) 0143 if isinstance(result.value, int): 0144 if 0 <= result.value <= 11: 0145 str.__setattr__( 0146 result, 0147 'prevForChow', 0148 Tile(result.group, 0149 result.value - 1)) 0150 if -1 <= result.value <= 10: 0151 str.__setattr__( 0152 result, 0153 'nextForChow', 0154 Tile( 0155 result.group, 0156 result.value + 0157 1)) 0158 0159 return result 0160 0161 def __getitem__(self, index): 0162 if hasattr(self, '_fixed'): 0163 raise TypeError 0164 return str.__getitem__(self, index) 0165 0166 def __setitem__(self, index, value): 0167 raise TypeError 0168 0169 def __delitem__(self, index): 0170 raise TypeError 0171 0172 def lower(self): 0173 raise TypeError 0174 0175 def istitle(self): 0176 raise TypeError 0177 0178 def upper(self): 0179 raise TypeError 0180 0181 def capitalize(self): 0182 raise TypeError 0183 0184 def meld(self, size): 0185 """return a meld of size. Those attributes are set 0186 in Meld.cacheMeldsInTiles""" 0187 return getattr(self, ('single', 'pair', 'pung', 'kong')[size - 1]) 0188 0189 def groupName(self): 0190 """the name of the group this tile is of""" 0191 names = { 0192 Tile.hidden: i18nc('kajongg', 'hidden'), 0193 Tile.stone: i18nc('kajongg', 'stone'), 0194 Tile.bamboo: i18nc('kajongg', 'bamboo'), 0195 Tile.character: i18nc('kajongg', 'character'), 0196 Tile.wind: i18nc('kajongg', 'wind'), 0197 Tile.dragon: i18nc('kajongg', 'dragon'), 0198 Tile.flower: i18nc('kajongg', 'flower'), 0199 Tile.season: i18nc('kajongg', 'season')} 0200 return names[self.lowerGroup] 0201 0202 def valueName(self): 0203 """the name of the value this tile has""" 0204 names = { 0205 'y': i18nc('kajongg', 'tile'), 0206 Tile.white: i18nc('kajongg', 'white'), 0207 Tile.red: i18nc('kajongg', 'red'), 0208 Tile.green: i18nc('kajongg', 'green'), 0209 East: i18nc('kajongg', 'East'), 0210 South: i18nc('kajongg', 'South'), 0211 West: i18nc('kajongg', 'West'), 0212 North: i18nc('kajongg', 'North')} 0213 for idx in Tile.numbers: 0214 names[idx] = chr(idx + 48) 0215 return names[self.value] 0216 0217 def name(self): 0218 """return name of a single tile""" 0219 if self.group.lower() == Tile.wind: 0220 result = { 0221 East: i18n('East Wind'), 0222 South: i18n('South Wind'), 0223 West: i18n('West Wind'), 0224 North: i18n('North Wind')}[self.value] 0225 else: 0226 result = i18nc('kajongg tile name', '{group} {value}') 0227 return result.format(value=self.valueName(), group=self.groupName()) 0228 0229 def __lt__(self, other): 0230 """needed for sort""" 0231 return self.key < other.key 0232 0233 0234 class TileList(list): 0235 0236 """a list that can only hold tiles""" 0237 0238 def __init__(self, newContent=None): 0239 list.__init__(self) 0240 if newContent is None: 0241 return 0242 if isinstance(newContent, Tile): 0243 list.append(self, newContent) 0244 elif isinstance(newContent, str): 0245 list.extend( 0246 self, [Tile(newContent[x:x + 2]) 0247 for x in range(0, len(newContent), 2)]) 0248 else: 0249 list.extend(self, newContent) 0250 self.isRest = True 0251 0252 def key(self): 0253 """usable for sorting""" 0254 result = 0 0255 factor = len(Tile.hashTable) // 2 0256 for tile in self: 0257 result = result * factor + tile.key 0258 return result 0259 0260 def sorted(self): 0261 """sort(TileList) would not keep TileList type""" 0262 return TileList(sorted(self)) 0263 0264 def hasChows(self, tile): 0265 """return my chows with tileName""" 0266 if tile not in self: 0267 return [] 0268 if tile.lowerGroup not in Tile.colors: 0269 return [] 0270 group = tile.group 0271 values = {x.value for x in self if x.group == group} 0272 chows = [] 0273 for offsets in [(0, 1, 2), (-2, -1, 0), (-1, 0, 1)]: 0274 subset = {tile.value + x for x in offsets} 0275 if subset <= values: 0276 chow = TileList(Tile(group, x) for x in sorted(subset)) 0277 if chow not in chows: 0278 chows.append(chow) 0279 return chows 0280 0281 def __str__(self): 0282 """the content""" 0283 return str(''.join(self)) 0284 0285 0286 class Elements: 0287 0288 """represents all elements""" 0289 # pylint: disable=too-many-instance-attributes 0290 # too many attributes 0291 0292 def __init__(self): 0293 self.occurrence = IntDict() # key: db, s3 etc. value: occurrence 0294 self.winds = {Tile(Tile.wind, x) for x in Tile.winds} 0295 self.wINDS = {x.concealed for x in self.winds} 0296 self.dragons = {Tile(Tile.dragon, x) for x in Tile.dragons} 0297 self.dRAGONS = {x.concealed for x in self.dragons} 0298 self.honors = self.winds | self.dragons 0299 self.hONORS = self.wINDS | self.dRAGONS 0300 self.terminals = {Tile(x, y) 0301 for x in Tile.colors for y in Tile.terminals} 0302 self.tERMINALS = {x.concealed for x in self.terminals} 0303 self.majors = self.honors | self.terminals 0304 self.mAJORS = self.hONORS | self.tERMINALS 0305 self.greenHandTiles = { 0306 Tile(Tile.bamboo, x) 0307 for x in '23468'} | {Tile(Tile.dragon, Tile.green)} 0308 self.minors = {Tile(x, y) for x in Tile.colors for y in Tile.minors} 0309 for tile in self.majors: 0310 self.occurrence[tile] = 4 0311 for tile in self.minors: 0312 self.occurrence[tile] = 4 0313 for bonus in Tile.boni: 0314 for _ in Tile.winds: 0315 self.occurrence[Tile(bonus, _)] = 1 0316 0317 def __filter(self, ruleset): 0318 """return element names""" 0319 return (x for x in self.occurrence 0320 if ruleset.withBonusTiles or (not x.isBonus)) 0321 0322 def count(self, ruleset): 0323 """how many tiles are to be used by the game""" 0324 return self.occurrence.count(self.__filter(ruleset)) 0325 0326 def all(self, ruleset): 0327 """a list of all elements, each of them occurrence times""" 0328 return self.occurrence.all(self.__filter(ruleset)) 0329 0330 Tile.unknown = Tile('Xy') # must come first 0331 elements = Elements() # pylint: disable=invalid-name 0332 assert not Tile.unknown.isKnown 0333 for wind in Wind.all4: 0334 wind.tile = Tile('w', wind.char.lower())