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