#!/usr/bin/env python # -*- mode: python; coding: utf-8; -*- # ---------------------------------------------------------------------------## # # Copyright (C) 1998-2003 Markus Franz Xaver Johannes Oberhumer # Copyright (C) 2003 Mt. Hood Playing Card Co. # Copyright (C) 2005-2009 Skomoroh # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # ---------------------------------------------------------------------------## import glob import os import traceback from pysollib.mfxutil import Image, KwStruct, Struct, USE_PIL from pysollib.mygettext import _ from pysollib.settings import DEBUG # ************************************************************************ # * Abstract # ************************************************************************ class Resource(Struct): def __init__(self, **kw): kw = KwStruct( kw, name="", filename="", basename="", # basename of filename absname="", # absolute filename # implicit index=-1, error=0, # error while loading this resource ) Struct.__init__(self, **kw.getKw()) def getSortKey(self): return self.name.lower() class ResourceManager: def __init__(self): self._selected_key = -1 self._objects = [] self._objects_by_name = None self._objects_cache_name = {} self._objects_cache_filename = {} self._objects_cache_basename = {} self._objects_cache_absname = {} def getSelected(self): return self._selected_key def setSelected(self, index): assert -1 <= index < len(self._objects) self._selected_key = index def len(self): return len(self._objects) def register(self, obj): assert obj.index == -1 assert obj.name and obj.name not in self._objects_cache_name self._objects_cache_name[obj.name] = obj if obj.filename: obj.absname = os.path.abspath(obj.filename) obj.basename = os.path.basename(obj.filename) self._objects_cache_filename[obj.filename] = obj self._objects_cache_basename[obj.basename] = obj self._objects_cache_absname[obj.absname] = obj obj.index = len(self._objects) self._objects.append(obj) self._objects_by_name = None # invalidate def get(self, index): if 0 <= index < len(self._objects): return self._objects[index] return None def getByName(self, key): return self._objects_cache_name.get(key) def getByBasename(self, key): return self._objects_cache_basename.get(key) def getAll(self): return tuple(self._objects) def getAllSortedByName(self): if self._objects_by_name is None: lst = [(obj.getSortKey(), obj) for obj in self._objects] lst.sort() self._objects_by_name = tuple([item[1] for item in lst]) return self._objects_by_name # # static methods # def _addDir(self, result, dir): try: if dir: dir = os.path.normpath(dir) if dir and os.path.isdir(dir) and dir not in result: result.append(dir) except EnvironmentError: pass def getSearchDirs(self, app, search, env=None): """Get a list of normalized directory paths. The returned list has no duplicates.""" if isinstance(search, str): search = (search,) result = [] if env: for d in os.environ.get(env, "").split(os.pathsep): self._addDir(result, d.strip()) for dir in (app.dataloader.dir, app.dn.maint, app.dn.config): if not dir: continue dir = os.path.normpath(dir) if not dir or not os.path.isdir(dir): continue for s in search: try: if s[-2:] == "-*": d = os.path.normpath(os.path.join(dir, s[:-2])) self._addDir(result, d) globdirs = glob.glob(d + "-*") globdirs.sort() for d in globdirs: self._addDir(result, d) else: self._addDir(result, os.path.join(dir, s)) except EnvironmentError: traceback.print_exc() pass if DEBUG >= 6: print("getSearchDirs", env, search, "->", result) return result # ************************************************************************ # * Cardset # ************************************************************************ # CardsetInfo constants class CSI: # cardset size SIZE_TINY = 1 SIZE_SMALL = 2 SIZE_MEDIUM = 3 SIZE_LARGE = 4 SIZE_XLARGE = 5 SIZE_HIRES = 6 # cardset types TYPE_FRENCH = 1 TYPE_HANAFUDA = 2 TYPE_TAROCK = 3 TYPE_MAHJONGG = 4 TYPE_HEXADECK = 5 TYPE_MUGHAL_GANJIFA = 6 TYPE_NAVAGRAHA_GANJIFA = 7 TYPE_DASHAVATARA_GANJIFA = 8 TYPE_TRUMP_ONLY = 9 TYPE_MATCHING = 10 TYPE_PUZZLE = 11 TYPE_ISHIDO = 12 # cardset subtypes # (french) SUBTYPE_NONE = 0 SUBTYPE_JOKER_DECK = 1 # (puzzle) SUBTYPE_3X3 = 3 SUBTYPE_4X4 = 4 SUBTYPE_5X5 = 5 SUBTYPE_6X6 = 6 SUBTYPE_7X7 = 7 SUBTYPE_8X8 = 8 SUBTYPE_9X9 = 9 SUBTYPE_10X10 = 10 TYPE = { 1: _("French type (52-54 cards)"), 2: _("Hanafuda type (48 cards)"), 3: _("Tarock type (78 cards)"), 4: _("Mahjongg type (42 tiles)"), 5: _("Hex A Deck type (68 cards)"), 6: _("Mughal Ganjifa type (96 cards)"), 7: _("Navagraha Ganjifa type (108 cards)"), 8: _("Dashavatara Ganjifa type (120 cards)"), 9: _("Trumps only type (variable cards)"), 10: _("Matching type (variable cards)"), 11: _("Puzzle type (variable pieces)"), 12: _("Ishido type (36 tiles)") } TYPE_NAME = { 1: _("French"), 2: _("Hanafuda"), 3: _("Tarock"), 4: _("Mahjongg"), 5: _("Hex A Deck"), 6: _("Mughal Ganjifa"), 7: _("Navagraha Ganjifa"), 8: _("Dashavatara Ganjifa"), 9: _("Trumps only"), 10: _("Matching"), 11: _("Puzzle"), 12: _("Ishido") } SUBTYPE_NAME = { 1: {0: _("No Jokers"), 1: _("Joker Deck")}, 11: {3: _("3x3"), 4: _("4x4"), 5: _("5x5"), 6: _("6x6"), 7: _("7x7"), 8: _("8x8"), 9: _("9x9"), 10: _("10x10")} } TYPE_ID = { 1: "french", 2: "hanafuda", 3: "tarock", 4: "mahjongg", 5: "hex-a-deck", 6: "mughal-ganjifa", 7: "navagraha-ganjifa", 8: "dashavatara-ganjifa", 9: "trumps-only", 10: "matching", 11: "puzzle", 12: "ishido" } TYPE_SUITS = { 1: "cshd", 2: "abcdefghijkl", 3: "cshd", 4: "abc", 5: "cshd", 6: "abcdefgh", 7: "abcdefghi", 8: "abcdefghij", 9: "", 10: "", 11: "", 12: "abcdef" } TYPE_RANKS = { 1: list(range(13)), 2: list(range(4)), 3: list(range(14)), 4: list(range(10)), 5: list(range(16)), 6: list(range(12)), 7: list(range(12)), 8: list(range(12)), 9: list(range(0)), 10: list(range(0)), 11: list(range(0)), 12: list(range(6)) } TYPE_TRUMPS = { 1: (), 2: (), 3: list(range(22)), 4: list(range(12)), 5: list(range(4)), 6: (), 7: (), 8: (), 9: (), 10: (), 11: (), 12: () } # cardset styles STYLE = { 35: _("Abstract"), # 1: _("Adult"), # 2: _("Animals"), # 3: _("Anime"), # 4: _("Art"), # 5: _("Cartoons"), # 6: _("Children"), # 7: _("Classic Look"), # 8: _("Collectors"), # scanned collectors cardsets 9: _("Computers"), # 36: _("Divination"), # e.g. fortunetelling decks 10: _("Engines"), # 11: _("Fantasy"), # 37: _("Four Color"), # 30: _("Ganjifa"), # 12: _("Hanafuda"), # 29: _("Hex A Deck"), # 13: _("Holiday"), # 34: _("Ishido"), # 28: _("Mahjongg"), # 32: _("Matching"), # 38: _("Monochrome"), # 14: _("Movies"), # 31: _("Matrix"), # 15: _("Music"), # 16: _("Nature"), # 17: _("Operating Systems"), # e.g. cards with Linux logos 19: _("People"), # famous people 20: _("Places"), # 21: _("Plain"), # 22: _("Products"), # 33: _("Puzzle"), # 18: _("Round Cardsets"), # 23: _("Science Fiction"), # 24: _("Sports"), # 27: _("Tarock"), # 25: _("Vehicles"), # 26: _("Video Games"), # } # cardset nationality (suit and rank symbols) NATIONALITY = { 1021: _("Australia"), # 1001: _("Austria"), # 1019: _("Belgium"), # 1010: _("Canada"), # 1011: _("China"), # 1012: _("Czech Republic"), # 1013: _("Denmark"), # 1003: _("England"), # 1004: _("France"), # 1006: _("Germany"), # 1014: _("Great Britain"), # 1015: _("Hungary"), # 1020: _("India"), # 1005: _("Italy"), # 1016: _("Japan"), # 1002: _("Netherlands"), # 1022: _("Portugal"), # 1007: _("Russia"), # 1008: _("Spain"), # 1017: _("Sweden"), # 1009: _("Switzerland"), # 1018: _("USA"), # } # cardset creation date DATE = { 10: "1000 - 1099", 11: "1100 - 1199", 12: "1200 - 1299", 13: "1300 - 1399", 14: "1400 - 1499", 15: "1500 - 1599", 16: "1600 - 1699", 17: "1700 - 1799", 18: "1800 - 1899", 19: "1900 - 1999", 20: "2000 - 2099", 21: "2100 - 2199", 22: "2200 - 2299", } class CardsetConfig(Struct): # see config.txt and _readCardsetConfig() def __init__(self): Struct.__init__( self, # line[0] version=1, ext=".gif", type=CSI.TYPE_FRENCH, ncards=-1, styles=[], year=0, subtype=0, mahjongg3d=False, # line[1] ident="", name="", # line[2] CARDW=0, CARDH=0, CARDD=0, # line[3] CARD_XOFFSET=0, CARD_YOFFSET=0, SHADOW_XOFFSET=0, SHADOW_YOFFSET=0, # line[4] backindex=0, # line[5] backnames=(), # other CARD_DX=0, # relative pos of real card image within Card CARD_DY=0, ) class Cardset(Resource): def __init__(self, **kw): # start with all fields from CardsetConfig config = CardsetConfig() kw = KwStruct(config.__dict__, **kw) # si is the SelectionInfo struct that will be queried by # the "select cardset" dialogs. It can be freely modified. si = Struct(type=0, subtype=0, size=0, styles=[], nationalities=[], dates=[]) kw = KwStruct( kw, # essentials ranks=(), suits=(), trumps=(), nbottoms=7, nletters=4, nshadows=1 + 13, # selection criteria si=si, # implicit backname=None, dir="", ) Resource.__init__(self, **kw.getKw()) def getFaceCardNames(self): names = [] for suit in self.suits: for rank in self.ranks: names.append("%02d%s" % (rank + 1, suit)) for trump in self.trumps: names.append("%02d%s" % (trump + 1, "z")) assert len(names) == self.ncards return names def getPreviewCardNames(self): names = self.getFaceCardNames() pnames = [] ranks, suits = self.ranks, self.suits lr, ls = len(ranks), len(suits) if lr == 0 or ls == 0: # TYPE_TRUMP_ONLY return names[:16], 4 if lr >= 4: ls = min(ls, 4) low_ranks, high_ranks = 1, 3 # if self.type == 3: high_ranks = 4 for rank in list(range(0, low_ranks)) + list(range(lr-high_ranks, lr)): for suit in range(ls): index = suit * len(self.ranks) + rank pnames.append(names[index % len(names)]) return pnames, ls def updateCardback(self, backname=None, backindex=None): # update default back if isinstance(backname, str): if backname in self.backnames: backindex = self.backnames.index(backname) if isinstance(backindex, int): self.backindex = backindex % len(self.backnames) self.backname = self.backnames[self.backindex] def saveSettings(self): print('saveSettings') class CardsetManager(ResourceManager): def __init__(self): ResourceManager.__init__(self) self.registered_types = {} self.registered_subtypes = {} self.type_max_cards = {} self.registered_sizes = {} self.registered_styles = {} self.registered_nationalities = {} self.registered_dates = {} self.uncategorized_styles = False self.uncategorized_nationalities = False self.uncategorized_dates = False def _check(self, cs): s = cs.type if s not in CSI.TYPE: return 0 cs.si.type = s cs.si.subtype = cs.subtype cs.suits = CSI.TYPE_SUITS[s] cs.ranks = CSI.TYPE_RANKS[s] cs.trumps = CSI.TYPE_TRUMPS[s] if s == CSI.TYPE_FRENCH: if cs.subtype == 1: cs.trumps = list(range(2)) cs.nbottoms = 8 elif s == CSI.TYPE_HANAFUDA: cs.nbottoms = 15 elif s == CSI.TYPE_TAROCK: cs.nbottoms = 8 elif s == CSI.TYPE_MAHJONGG: cs.nbottoms = 0 cs.nletters = 0 cs.nshadows = 0 elif s == CSI.TYPE_HEXADECK: cs.nbottoms = 8 elif s == CSI.TYPE_MUGHAL_GANJIFA: cs.nbottoms = 11 elif s == CSI.TYPE_NAVAGRAHA_GANJIFA: # ???return 0 ## FIXME cs.nbottoms = 12 elif s == CSI.TYPE_DASHAVATARA_GANJIFA: cs.nbottoms = 13 elif s == CSI.TYPE_TRUMP_ONLY: # ???return 0 ## FIXME # cs.nbottoms = 7 # cs.ranks = () # cs.suits = "" # cs.trumps = range(cs.ncards) cs.nbottoms = 1 cs.nletters = 0 cs.nshadows = 0 cs.trumps = list(range(cs.ncards)) elif s == CSI.TYPE_MATCHING: # ???return 0 ## FIXME # cs.nbottoms = 7 # cs.ranks = () # cs.suits = "" # cs.trumps = range(cs.ncards) cs.nbottoms = 1 cs.nletters = 0 cs.nshadows = 0 cs.trumps = list(range(cs.ncards)) elif s == CSI.TYPE_PUZZLE: # ???return 0 ## FIXME # cs.nbottoms = 7 # cs.ranks = () # cs.suits = "" # cs.trumps = range(cs.ncards) cs.nbottoms = 1 cs.nletters = 0 cs.nshadows = 0 cs.trumps = list(range(cs.ncards)) elif s == CSI.TYPE_ISHIDO: cs.nbottoms = 1 cs.nletters = 0 cs.nshadows = 0 else: return 0 return 1 def register(self, cs): if not self._check(cs): return cs.ncards = len(cs.ranks) * len(cs.suits) + len(cs.trumps) cs.name = cs.name[:30] if not (CSI.SIZE_TINY <= cs.si.size <= CSI.SIZE_HIRES): CW, CH = cs.CARDW, cs.CARDH if CW <= 55 and CH <= 72: cs.si.size = CSI.SIZE_TINY elif CW <= 60 and CH <= 85: cs.si.size = CSI.SIZE_SMALL elif CW <= 75 and CH <= 105: cs.si.size = CSI.SIZE_MEDIUM elif CW <= 90 and CH <= 125: cs.si.size = CSI.SIZE_LARGE elif CW <= 150 and CH <= 210: cs.si.size = CSI.SIZE_XLARGE else: cs.si.size = CSI.SIZE_HIRES # keys = cs.styles[:] cs.si.styles = tuple([s for s in keys if s in CSI.STYLE]) if len(cs.si.styles) == 0: self.uncategorized_styles = True for s in cs.si.styles: self.registered_styles[s] = self.registered_styles.get(s, 0) + 1 cs.si.nationalities = tuple([s for s in keys if s in CSI.NATIONALITY]) if len(cs.si.nationalities) == 0: self.uncategorized_nationalities = True for s in cs.si.nationalities: self.registered_nationalities[s] = \ self.registered_nationalities.get(s, 0) + 1 if cs.year == 0: self.uncategorized_dates = True keys = (cs.year // 100,) cs.si.dates = tuple([s for s in keys if s in CSI.DATE]) for s in cs.si.dates: self.registered_dates[s] = self.registered_dates.get(s, 0) + 1 # s = cs.si.type self.registered_types[s] = self.registered_types.get(s, 0) + 1 if self.registered_types[s] == 1: self.registered_subtypes[s] = {} ss = cs.si.subtype self.registered_subtypes[s][ss] = \ self.registered_subtypes.get(s, 0).get(ss, 0) + 1 if s not in self.type_max_cards or self.type_max_cards[s] < cs.ncards: self.type_max_cards[s] = cs.ncards s = cs.si.size self.registered_sizes[s] = self.registered_sizes.get(s, 0) + 1 cs.updateCardback() ResourceManager.register(self, cs) def identify_missing_cardsets(self): missing = [] # This object should list the bare minimum cardset requirements # for a PySol install that can play all games. required_types = { CSI.TYPE_FRENCH: { CSI.SUBTYPE_JOKER_DECK }, CSI.TYPE_HANAFUDA: {}, CSI.TYPE_TAROCK: {}, CSI.TYPE_MAHJONGG: {}, CSI.TYPE_HEXADECK: {}, CSI.TYPE_MUGHAL_GANJIFA: {}, CSI.TYPE_DASHAVATARA_GANJIFA: {}, CSI.TYPE_TRUMP_ONLY: {}, CSI.TYPE_PUZZLE: { CSI.SUBTYPE_3X3, CSI.SUBTYPE_4X4, CSI.SUBTYPE_5X5, CSI.SUBTYPE_6X6, CSI.SUBTYPE_7X7, CSI.SUBTYPE_8X8, CSI.SUBTYPE_9X9, CSI.SUBTYPE_10X10 }, CSI.TYPE_ISHIDO: {} } required_cards_needed = { CSI.TYPE_TRUMP_ONLY: 100 } for t in required_types.keys(): if t not in self.registered_types: missing.append(CSI.TYPE_NAME[t]) else: if len(required_types[t]) > 0: for tt in required_types[t]: if tt not in self.registered_subtypes[t]: missing.append(CSI.TYPE_NAME[t] + " (" + CSI.SUBTYPE_NAME[t][tt] + ")") if t in required_cards_needed: if self.type_max_cards[t] < required_cards_needed[t]: missing.append(CSI.TYPE_NAME[t] + " (" + _("With %(cards)d or more cards" + ")") % {'cards': required_cards_needed[t]}) missing.sort() return missing # ************************************************************************ # * Tile # ************************************************************************ # TableTileInfo constants class TTI: # tile size SIZE_UNKNOWN = 0 SIZE_TILE = 1 SIZE_SD = 2 SIZE_HD = 3 SIZE_4K = 4 class Tile(Resource): def __init__(self, **kw): kw['color'] = None kw['stretch'] = 0 kw['save_aspect'] = 0 kw['size'] = 0 Resource.__init__(self, **kw) class TileManager(ResourceManager): def register(self, tile): if USE_PIL: try: img = Image.open(tile.filename) TW, TH = img.size if TW < 640 or TH < 480: tile.size = TTI.SIZE_TILE elif TW < 1280 or TH < 720: tile.size = TTI.SIZE_SD elif TW < 3840 or TH < 2160: tile.size = TTI.SIZE_HD else: tile.size = TTI.SIZE_4K except AttributeError: tile.size = TTI.SIZE_UNKNOWN else: tile.size = TTI.SIZE_UNKNOWN ResourceManager.register(self, tile) # ************************************************************************ # * Sample # ************************************************************************ class Sample(Resource): def __init__(self, **kw): kw['volume'] = -1 Resource.__init__(self, **kw) class SampleManager(ResourceManager): pass # ************************************************************************ # * Music # ************************************************************************ class Music(Sample): pass class MusicManager(SampleManager): pass