File indexing completed on 2024-04-28 07:51:10
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 import types 0011 from hashlib import md5 0012 0013 from common import Internal, Debug 0014 from common import StrMixin 0015 from log import logException, logDebug 0016 from mi18n import i18n, i18nc, i18nE, i18ncE, english 0017 from query import Query 0018 0019 0020 class Score(StrMixin): 0021 0022 """holds all parts contributing to a score. It has two use cases: 0023 1. for defining what a rules does: either points or doubles or limits, holding never more than one unit 0024 2. for summing up the scores of all rules: Now more than one of the units can be in use. If a rule 0025 should want to set more than one unit, split it into two rules. 0026 For the first use case only we have the attributes value and unit""" 0027 0028 __hash__ = None 0029 0030 def __init__(self, points=0, doubles=0, limits=0, ruleset=None): 0031 self.points = 0 # define the types for those values 0032 self.doubles = 0 0033 self.limits = 0.0 0034 self.ruleset = ruleset 0035 self.points = type(self.points)(points) 0036 self.doubles = type(self.doubles)(doubles) 0037 self.limits = type(self.limits)(limits) 0038 0039 unitNames = {i18nE( 0040 'points'): 0, 0041 i18ncE('kajongg', 'doubles'): 50, 0042 i18ncE('kajongg', 'limits'): 9999} 0043 0044 def clear(self): 0045 """set all to 0""" 0046 self.points = self.doubles = self.limits = 0 0047 0048 def change(self, unitName, value): 0049 """set value for unitName. If changed, return True""" 0050 oldValue = self.__getattribute__(unitName) 0051 newValue = type(oldValue)(value) 0052 if newValue == oldValue: 0053 return False, None 0054 if newValue: 0055 if unitName == 'points': 0056 if self.doubles: 0057 return False, 'Cannot have points and doubles' 0058 if unitName == 'doubles': 0059 if self.points: 0060 return False, 'Cannot have points and doubles' 0061 self.__setattr__(unitName, newValue) 0062 return True, None 0063 0064 def __str__(self): 0065 """make score printable""" 0066 parts = [] 0067 if self.points: 0068 parts.append('points=%d' % self.points) 0069 if self.doubles: 0070 parts.append('doubles=%d' % self.doubles) 0071 if self.limits: 0072 parts.append('limits=%f' % self.limits) 0073 return ' '.join(parts) 0074 0075 def i18nStr(self): 0076 """make score readable for humans, i18n""" 0077 parts = [] 0078 if self.points: 0079 parts.append(i18nc('Kajongg', '%1 points', self.points)) 0080 if self.doubles: 0081 parts.append(i18nc('Kajongg', '%1 doubles', self.doubles)) 0082 if self.limits: 0083 limits = str(self.limits) 0084 if limits.endswith('.0'): 0085 limits = limits[-2:] 0086 parts.append(i18nc('Kajongg', '%1 limits', limits)) 0087 return ' '.join(parts) 0088 0089 def __eq__(self, other): 0090 """ == comparison """ 0091 assert isinstance(other, Score) 0092 return self.points == other.points and self.doubles == other.doubles and self.limits == other.limits 0093 0094 def __ne__(self, other): 0095 """ != comparison """ 0096 return self.points != other.points or self.doubles != other.doubles or self.limits != other.limits 0097 0098 def __lt__(self, other): 0099 return self.total() < other.total() 0100 0101 def __le__(self, other): 0102 return self.total() <= other.total() 0103 0104 def __gt__(self, other): 0105 return self.total() > other.total() 0106 0107 def __ge__(self, other): 0108 return self.total() >= other.total() 0109 0110 def __add__(self, other): 0111 """implement adding Score""" 0112 return Score(self.points + other.points, self.doubles + other.doubles, 0113 max(self.limits, other.limits), self.ruleset or other.ruleset) 0114 0115 def total(self): 0116 """the total score""" 0117 score = int(self.points * (2 ** self.doubles)) 0118 if self.limits: 0119 if self.limits >= 1: 0120 self.points = self.doubles = 0 0121 elif self.limits * self.ruleset.limit >= score: 0122 self.points = self.doubles = 0 0123 else: 0124 self.limits = 0 0125 if self.limits: 0126 return int(round(self.limits * self.ruleset.limit)) 0127 if score and not self.ruleset.roofOff: 0128 score = min(score, self.ruleset.limit) 0129 return score 0130 0131 def __int__(self): 0132 """the total score""" 0133 return self.total() 0134 0135 def __nonzero__(self): 0136 """for bool() conversion""" 0137 return self.points != 0 or self.doubles != 0 or self.limits != 0 0138 0139 0140 class RuleList(list): 0141 0142 """a list with a name and a description (to be used as hint). 0143 Rules can be indexed by name or index. 0144 Adding a rule either replaces an existing rule or appends it.""" 0145 0146 def __init__(self, listId, name, description): 0147 list.__init__(self) 0148 self.listId = listId 0149 self.name = name 0150 self.description = description 0151 0152 def pop(self, key): 0153 """find rule, return it, delete it from this list""" 0154 result = self.__getitem__(key) 0155 self.__delitem__(key) 0156 return result 0157 0158 def __contains__(self, key): 0159 """do we know this rule?""" 0160 if isinstance(key, RuleBase): 0161 key = key.key() 0162 return any(x.key() == key for x in self) 0163 0164 def __getitem__(self, key): 0165 """find rule by key""" 0166 if isinstance(key, int): 0167 return list.__getitem__(self, key) 0168 for rule in self: 0169 if rule.key() == key: 0170 return rule 0171 raise KeyError 0172 0173 def __setitem__(self, key, rule): 0174 """set rule by key""" 0175 if isinstance(key, int): 0176 list.__setitem__(self, key, rule) 0177 return 0178 for idx, oldRule in enumerate(self): 0179 if oldRule.key() == key: 0180 list.__setitem__(self, idx, rule) 0181 return 0182 list.append(self, rule) 0183 0184 def __delitem__(self, key): 0185 """delete this rule""" 0186 if isinstance(key, int): 0187 list.__delitem__(self, key) 0188 return 0189 for idx, rule in enumerate(self): 0190 if rule.key() == key: 0191 list.__delitem__(self, idx) 0192 return 0193 raise KeyError 0194 0195 def append(self, rule): 0196 """do not append""" 0197 raise Exception('do not append %s' % rule) 0198 0199 def add(self, rule): 0200 """use add instead of append""" 0201 if rule.key() in self: 0202 logException('%s is already defined as %s, not accepting new rule %s/%s' % ( 0203 rule.key(), self[rule.key()].definition, rule.name, rule.definition)) 0204 self[rule.key()] = rule 0205 0206 def createRule(self, name: str, definition: str = '', **kwargs): 0207 """shortcut for simpler definition of predefined rulesets""" 0208 defParts = definition.split('||') 0209 rule = None 0210 description = kwargs.get('description', '') 0211 for cls in [IntRule, BoolRule, StrRule]: 0212 if defParts[0].startswith(cls.prefix): 0213 rule = cls( 0214 name, 0215 definition, 0216 description=description, 0217 parameter=kwargs['parameter']) 0218 break 0219 if not rule: 0220 if 'parameter' in kwargs: 0221 del kwargs['parameter'] 0222 ruleType = type(ruleKey(name) + 'Rule', (Rule, ), {}) 0223 rule = ruleType(name, definition, **kwargs) 0224 if defParts[0] == 'FCallingHand': 0225 parts1 = defParts[1].split('=') 0226 assert parts1[0] == 'Ohand', definition 0227 ruleClassName = parts1[1] + 'Rule' 0228 if ruleClassName not in RuleBase.ruleClasses: 0229 logDebug( 0230 'we want %s, definition:%s' % 0231 (ruleClassName, definition)) 0232 logDebug('we have %s' % RuleBase.ruleClasses.keys()) 0233 ruleType.limitHand = RuleBase.ruleClasses[ruleClassName] 0234 self.add(rule) 0235 0236 0237 class UsedRule(StrMixin): 0238 0239 """use this in scoring, never change class Rule. 0240 If the rule has been used for a meld, pass it""" 0241 0242 def __init__(self, rule, meld=None): 0243 self.rule = rule 0244 self.meld = meld 0245 0246 def __str__(self): 0247 result = self.rule.name 0248 if self.meld: 0249 result += ' ' + str(self.meld) 0250 return result 0251 0252 0253 class Ruleset: 0254 0255 """holds a full set of rules: meldRules,handRules,winnerRules. 0256 0257 predefined rulesets are preinstalled together with Kajongg. They can be customized by the user: 0258 He can copy them and modify the copies in any way. If a game uses a specific ruleset, it 0259 checks the used rulesets for an identical ruleset and refers to that one, or it generates 0260 a new used ruleset. 0261 0262 The user can select any predefined or customized ruleset for a new game, but she can 0263 only modify customized rulesets. 0264 0265 For fast comparison for equality of two rulesets, each ruleset has a hash built from 0266 all of its rules. This excludes the splitting rules, IOW exactly the rules saved in the table 0267 rule will be used for computation. 0268 0269 Rulesets which are templates for new games have negative ids: The ruleset editor only reads 0270 and writes rulesets with negative id. 0271 Rulesets attached to a game have positive ids. 0272 This can lead to a situation where the same ruleset is twice in the table: 0273 1. clear database 0274 2. play ruleset X which saves it with id=1 0275 3. in the ruleset editor, copy X which saves it with id=-1 and name='Copy of X' 0276 0277 The name is not unique. Different remote players might save different rulesets 0278 under the same name. 0279 """ 0280 # pylint: disable=too-many-instance-attributes 0281 0282 __hash__ = None 0283 0284 cache = dict() 0285 hits = 0 0286 misses = 0 0287 0288 @staticmethod 0289 def cached(name): 0290 """If a Ruleset instance is never changed, we can use a cache""" 0291 if isinstance(name, list): 0292 # we got the rules over the wire 0293 _, wiredHash, _, _ = name[0] 0294 else: 0295 wiredHash = None 0296 for predefined in PredefinedRuleset.rulesets(): 0297 if predefined.hash in (name, wiredHash): 0298 return predefined 0299 cache = Ruleset.cache 0300 if not isinstance(name, list) and name in cache: 0301 return cache[name] 0302 result = Ruleset(name) 0303 cache[result.rulesetId] = result 0304 cache[result.hash] = result 0305 return result 0306 0307 def __init__(self, name): 0308 """name may be: 0309 - an integer: ruleset.id from the sql table 0310 - a list: the full ruleset specification (probably sent from the server) 0311 - a string: The hash value of a ruleset""" 0312 Rule.importRulecode() 0313 self.name = name 0314 self.rulesetId = 0 0315 self.__hash = None 0316 self.allRules = [] 0317 self.__dirty = False # only the ruleset editor is supposed to make us dirty 0318 self.__loaded = False 0319 self.__filteredLists = {} 0320 self.description = None 0321 self.rawRules = None # used when we get the rules over the network 0322 self.doublingMeldRules = [] 0323 self.doublingHandRules = [] 0324 self.standardMJRule = None 0325 self.meldRules = RuleList(1, i18n('Meld Rules'), 0326 i18n('Meld rules are applied to single melds independent of the rest of the hand')) 0327 self.handRules = RuleList(2, i18n('Hand Rules'), 0328 i18n('Hand rules are applied to the entire hand, for all players')) 0329 self.winnerRules = RuleList(3, i18n('Winner Rules'), 0330 i18n('Winner rules are applied to the entire hand but only for the winner')) 0331 self.loserRules = RuleList(33, i18n('Loser Rules'), 0332 i18n('Loser rules are applied to the entire hand but only for non-winners')) 0333 self.mjRules = RuleList(4, i18n('Mah Jongg Rules'), 0334 i18n('Only hands matching a Mah Jongg rule can win')) 0335 self.parameterRules = RuleList(999, i18nc('kajongg', 'Options'), 0336 i18n('Here we have several special game related options')) 0337 self.penaltyRules = RuleList(9999, i18n('Penalties'), i18n( 0338 """Penalties are applied manually by the user. They are only used for scoring games. 0339 When playing against the computer or over the net, Kajongg will never let you get 0340 into a situation where you have to pay a penalty""")) 0341 self.ruleLists = list( 0342 [self.meldRules, self.handRules, self.mjRules, self.winnerRules, 0343 self.loserRules, self.parameterRules, self.penaltyRules]) 0344 # the order of ruleLists is the order in which the lists appear in the ruleset editor 0345 # if you ever want to remove an entry from ruleLists: make sure its listId is not reused or you get 0346 # in trouble when updating 0347 self._initRuleset() 0348 0349 @property 0350 def dirty(self): 0351 """have we been modified since load or last save?""" 0352 return self.__dirty 0353 0354 @dirty.setter 0355 def dirty(self, dirty): 0356 """have we been modified since load or last save?""" 0357 self.__dirty = dirty 0358 if dirty: 0359 self.__computeHash() 0360 0361 @property 0362 def hash(self): 0363 """a md5sum computed from the rules but not name and description""" 0364 if not self.__hash: 0365 self.__computeHash() 0366 return self.__hash 0367 0368 def __eq__(self, other): 0369 """two rulesets are equal if everything except name or description is identical. 0370 The name might be localized.""" 0371 return other and self.hash == other.hash 0372 0373 def __ne__(self, other): 0374 """two rulesets are equal if everything except name or description is identical. 0375 The name might be localized.""" 0376 return not other or self.hash != other.hash 0377 0378 def minMJTotal(self): 0379 """the minimum score for Mah Jongg including all winner points. This is not accurate, 0380 the correct number is bigger in CC: 22 and not 20. But it is enough saveguard against 0381 entering impossible scores for manual games. 0382 We only use this for scoring games.""" 0383 return self.minMJPoints + min(x.score.total() for x in self.mjRules) 0384 0385 @staticmethod 0386 def hashIsKnown(value): 0387 """return False or True""" 0388 result = any(x.hash == value for x in PredefinedRuleset.rulesets()) 0389 if not result: 0390 query = Query("select id from ruleset where hash=?", (value,)) 0391 result = bool(query.records) 0392 return result 0393 0394 def _initRuleset(self): 0395 """load ruleset headers but not the rules""" 0396 if isinstance(self.name, int): 0397 query = Query( 0398 "select id,hash,name,description from ruleset where id=?", (self.name,)) 0399 elif isinstance(self.name, list): 0400 # we got the rules over the wire 0401 self.rawRules = self.name[1:] 0402 (self.rulesetId, self.__hash, self.name, 0403 self.description) = self.name[0] 0404 self.load() 0405 # load raw rules at once, rules from db only when needed 0406 return 0407 else: 0408 query = Query("select id,hash,name,description from ruleset where hash=?", (self.name,)) 0409 if query.records: 0410 (self.rulesetId, self.__hash, self.name, 0411 self.description) = query.records[0] 0412 else: 0413 raise Exception('ruleset %s not found' % self.name) 0414 0415 def __setParametersFrom(self, fromRuleset): 0416 """set attributes for parameters defined in fromRuleset. 0417 Does NOT overwrite already set parameters: Silently ignore them""" 0418 for par in fromRuleset.parameterRules: 0419 if isinstance(par, ParameterRule): 0420 if par.parName not in self.__dict__: 0421 self.__dict__[par.parName] = par.parameter 0422 0423 def load(self): 0424 """load the ruleset from the database and compute the hash. Return self.""" 0425 if self.__loaded: 0426 return self 0427 self.__loaded = True 0428 self.loadRules() 0429 self.__setParametersFrom(self) 0430 for ruleList in self.ruleLists: 0431 assert len(ruleList) == len({x.key() 0432 for x in ruleList}), '%s has non-unique key' % ruleList.name 0433 for rule in ruleList: 0434 if hasattr(rule, 'score'): 0435 rule.score.ruleset = self 0436 self.allRules.append(rule) 0437 if self.rulesetId: # a saved ruleset, do not do this for predefined rulesets 0438 # we might have introduced new parameter rules which do not exist in this ruleset saved with the game, 0439 # so add missing parameters from the predefined ruleset most 0440 # similar to this one 0441 self.__setParametersFrom( 0442 sorted(PredefinedRuleset.rulesets(), 0443 key=lambda x: len(self.diff(x)))[0]) 0444 self.doublingMeldRules = [x for x in self.meldRules if x.score.doubles] 0445 self.doublingHandRules = [x for x in self.handRules if x.score.doubles] 0446 for mjRule in self.mjRules: 0447 if mjRule.__class__.__name__ == 'StandardMahJonggRule': 0448 self.standardMJRule = mjRule 0449 break 0450 assert self.standardMJRule 0451 return self 0452 0453 def __loadQuery(self): 0454 """return a Query object with loaded ruleset""" 0455 return Query( 0456 "select ruleset, list, position, name, definition, points, doubles, limits, parameter from rule " 0457 "where ruleset=? order by list,position", (self.rulesetId,)) 0458 0459 def toList(self): 0460 """return entire ruleset encoded in a string""" 0461 self.load() 0462 result = [[self.rulesetId, self.hash, self.name, self.description]] 0463 result.extend(self.ruleRecord(x) for x in self.allRules) 0464 return result 0465 0466 def loadRules(self): 0467 """load rules from database or from self.rawRules (got over the net)""" 0468 for record in self.rawRules or self.__loadQuery().records: 0469 self.__loadRule(record) 0470 0471 def __loadRule(self, record): 0472 """loads a rule into the correct ruleList""" 0473 _, listNr, _, name, definition, points, doubles, limits, parameter = record 0474 try: 0475 points = int(points) 0476 except ValueError: 0477 # this happens if the unit changed from limits to points but the value 0478 # is not converted at the same time 0479 points = int(float(points)) 0480 for ruleList in self.ruleLists: 0481 if ruleList.listId == listNr: 0482 ruleList.createRule( 0483 name, definition, points=points, doubles=int(doubles), limits=float(limits), 0484 parameter=parameter) 0485 break 0486 0487 def findUniqueOption(self, action): 0488 """return first rule with option""" 0489 rulesWithAction = [x for x in self.allRules if action in x.options] 0490 assert len(rulesWithAction) < 2, '%s has too many matching rules for %s' % ( 0491 str(self), action) 0492 if rulesWithAction: 0493 return rulesWithAction[0] 0494 return None 0495 0496 def filterRules(self, attrName): 0497 """return all my Rule classes having attribute attrName""" 0498 if attrName not in self.__filteredLists: 0499 self.__filteredLists[attrName] = [x for x in self.allRules if hasattr(x, attrName)] 0500 return self.__filteredLists[attrName] 0501 0502 @staticmethod 0503 def newId(minus=False): 0504 """return an unused ruleset id. This is not multi user safe.""" 0505 func = 'min(id)-1' if minus else 'max(id)+1' 0506 result = -1 if minus else 1 0507 records = Query("select %s from ruleset" % func).records 0508 if records and records[0] and records[0][0]: 0509 try: 0510 result = int(records[0][0]) 0511 except ValueError: 0512 pass 0513 return result 0514 0515 @staticmethod 0516 def nameExists(name): 0517 """return True if ruleset name is already in use""" 0518 result = any(x.name == name for x in PredefinedRuleset.rulesets()) 0519 if not result: 0520 result = bool( 0521 Query('select id from ruleset where id<0 and name=?', (name,)).records) 0522 return result 0523 0524 def _newKey(self, minus=False): 0525 """generate a new id and a new name if the name already exists""" 0526 newId = self.newId(minus=minus) 0527 newName = self.name 0528 if minus: 0529 copyNr = 1 0530 while self.nameExists(newName): 0531 copyStr = ' ' + str(copyNr) if copyNr > 1 else '' 0532 newName = i18nc( 0533 'Ruleset._newKey:%1 is empty or space plus number', 0534 'Copy%1 of %2', copyStr, i18n(self.name)) 0535 copyNr += 1 0536 return newId, newName 0537 0538 def clone(self): 0539 """return a clone of self, unloaded""" 0540 return Ruleset(self.rulesetId) 0541 0542 def __str__(self): 0543 return 'type=%s, id=%d,rulesetId=%d,name=%s' % ( 0544 type(self), id(self), self.rulesetId, self.name) 0545 0546 def copyTemplate(self): 0547 """make a copy of self and return the new ruleset id. Returns the new ruleset. 0548 To be used only for ruleset templates""" 0549 newRuleset = self.clone().load() 0550 newRuleset.save(minus=True, forced=True) 0551 if isinstance(newRuleset, PredefinedRuleset): 0552 newRuleset = Ruleset(newRuleset.rulesetId) 0553 return newRuleset 0554 0555 def __ruleList(self, rule): 0556 """return the list containing rule. We could make the list 0557 an attribute of the rule but then we rarely 0558 need this, and it is not time critical""" 0559 for ruleList in self.ruleLists: 0560 if rule in ruleList: 0561 return ruleList 0562 assert False 0563 return None 0564 0565 def rename(self, newName): 0566 """renames the ruleset. returns True if done, False if not""" 0567 with Internal.db: 0568 if self.nameExists(newName): 0569 return False 0570 query = Query( 0571 "update ruleset set name=? where id<0 and name=?", (newName, self.name)) 0572 if not query.failure: 0573 self.name = newName 0574 return not query.failure 0575 0576 def remove(self): 0577 """remove this ruleset from the database.""" 0578 with Internal.db: 0579 Query("DELETE FROM rule WHERE ruleset=?", (self.rulesetId,)) 0580 Query("DELETE FROM ruleset WHERE id=?", (self.rulesetId,)) 0581 0582 def __computeHash(self): 0583 """compute the hash for this ruleset using all rules but not name and 0584 description of the ruleset""" 0585 self.load() 0586 result = md5() 0587 for rule in sorted(self.allRules, key=Rule.__str__): 0588 result.update(rule.hashStr().encode('utf-8')) 0589 self.__hash = result.hexdigest() 0590 0591 def ruleRecord(self, rule): 0592 """return the rule as tuple, prepared for use by sql. The first three 0593 fields are the primary key.""" 0594 score = rule.score 0595 ruleList = None 0596 for ruleList in self.ruleLists: 0597 if rule in ruleList: 0598 ruleIdx = ruleList.index(rule) 0599 break 0600 assert rule in ruleList, '%s: %s not in list %s' % ( 0601 type(rule), rule, ruleList.name) 0602 return (self.rulesetId, ruleList.listId, ruleIdx, rule.name, 0603 rule.definition, score.points, score.doubles, score.limits, rule.parameter) 0604 0605 def updateRule(self, rule): 0606 """update rule in database""" 0607 self.__hash = None # invalidate, will be recomputed when needed 0608 with Internal.db: 0609 record = self.ruleRecord(rule) 0610 Query("UPDATE rule SET name=?, definition=?, points=?, doubles=?, limits=?, parameter=? " 0611 "WHERE ruleset=? AND list=? AND position=?", 0612 tuple(record[3:] + record[:3])) 0613 Query( 0614 "UPDATE ruleset SET hash=? WHERE id=?", 0615 (self.hash, 0616 self.rulesetId)) 0617 0618 def save(self, minus=False, forced=False): 0619 """save the ruleset to the database. 0620 If it does not yet exist in database, give it a new id 0621 If the name already exists in the database, also give it a new name 0622 If the hash already exists in the database, only save if forced=True""" 0623 if not forced: 0624 if minus: 0625 # if we save a template, only check for existing templates. Otherwise this could happen: 0626 # clear kajongg.db, play game with DMJL, start ruleset editor, copy DMJL. 0627 # since play Game saved the used ruleset with id 1, id 1 is found here and no new 0628 # template is generated. Next the ruleset editor shows the original ruleset in italics 0629 # and the copy with normal font but identical name, and the 0630 # copy is never saved. 0631 qData = Query( 0632 "select id from ruleset where hash=? and id<0", (self.hash,)).records 0633 else: 0634 qData = Query( 0635 "select id from ruleset where hash=?", (self.hash,)).records 0636 if qData: 0637 # is already in database 0638 self.rulesetId = qData[0][0] 0639 return 0640 with Internal.db: 0641 self.rulesetId, self.name = self._newKey(minus) 0642 Query( 0643 'INSERT INTO ruleset(id,name,hash,description) VALUES(?,?,?,?)', 0644 (self.rulesetId, english(self.name), 0645 self.hash, self.description), 0646 failSilent=True) 0647 cmd = 'INSERT INTO rule(ruleset, list, position, name, definition, ' \ 0648 'points, doubles, limits, parameter) VALUES(?,?,?,?,?,?,?,?,?)' 0649 args = [self.ruleRecord(x) for x in self.allRules] 0650 Query(cmd, args) 0651 0652 @staticmethod 0653 def availableRulesets(): 0654 """return all rulesets defined in the database plus all predefined rulesets""" 0655 templateIds = (x[0] 0656 for x in Query("SELECT id FROM ruleset WHERE id<0").records) 0657 result = [Ruleset(x) for x in templateIds] 0658 for predefined in PredefinedRuleset.rulesets(): 0659 if predefined not in result or predefined.name not in [x.name for x in result]: 0660 result.append(predefined) 0661 return result 0662 0663 @staticmethod 0664 def selectableRulesets(server=None): 0665 """return all selectable rulesets for a new game. 0666 server is used to find the last ruleset used by us on that server, this 0667 ruleset will returned first in the list.""" 0668 result = Ruleset.availableRulesets() 0669 # if we have a selectable ruleset with the same name as the last used ruleset 0670 # put that ruleset in front of the list. We do not want to use the exact same last used 0671 # ruleset because we might have made some fixes to the ruleset 0672 # meanwhile 0673 if server is None: # scoring game 0674 # the exists clause is only needed for inconsistent data bases 0675 qData = Query("select ruleset from game where seed is null " 0676 " and exists(select id from ruleset where game.ruleset=ruleset.id)" 0677 "order by starttime desc limit 1").records 0678 else: 0679 qData = Query( 0680 'select lastruleset from server where lastruleset is not null and url=?', 0681 (server,)).records 0682 if not qData: 0683 # we never played on that server 0684 qData = Query('select lastruleset from server where lastruleset is not null ' 0685 'order by lasttime desc limit 1').records 0686 if qData: 0687 lastUsedId = qData[0][0] 0688 qData = Query( 0689 "select name from ruleset where id=?", 0690 (lastUsedId, 0691 )).records 0692 if qData: 0693 lastUsed = qData[0][0] 0694 for idx, ruleset in enumerate(result): 0695 if ruleset.name == lastUsed: 0696 del result[idx] 0697 return [ruleset] + result 0698 return result 0699 0700 def diff(self, other): 0701 """return a list of tuples. Every tuple holds one or two rules: tuple[0] is from self, tuple[1] is from other""" 0702 result = [] 0703 leftDict = {x.name: x for x in self.allRules} 0704 rightDict = {x.name: x for x in other.allRules} 0705 left = set(leftDict.keys()) 0706 right = set(rightDict.keys()) 0707 for rule in left & right: 0708 leftRule, rightRule = leftDict[rule], rightDict[rule] 0709 if str(leftRule) != str(rightRule): 0710 result.append((leftRule, rightRule)) 0711 for rule in left - right: 0712 result.append((leftDict[rule], None)) 0713 for rule in right - left: 0714 result.append((None, rightDict[rule])) 0715 return result 0716 0717 0718 class RuleBase(StrMixin): 0719 0720 """a base for standard Rule and parameter rules IntRule, StrRule, BoolRule""" 0721 0722 options = {} 0723 ruleClasses = {} 0724 0725 def __init__(self, name: str, definition: str, description: str): 0726 self.hasSelectable = False 0727 self.ruleClasses[self.__class__.__name__] = self.__class__ 0728 self.__name = name 0729 self.definition = definition 0730 self.description = description 0731 0732 @property 0733 def name(self): 0734 """name is readonly""" 0735 return self.__name 0736 0737 def validate(self): # pylint: disable=no-self-use 0738 """is the rule valid?""" 0739 return True 0740 0741 def hashStr(self): # pylint: disable=no-self-use 0742 """ 0743 all that is needed to hash this rule 0744 0745 @return: The unique hash string 0746 @rtype: str 0747 """ 0748 return '' 0749 0750 def __str__(self): 0751 return self.hashStr() 0752 0753 0754 def ruleKey(name): 0755 """the key is used for finding a rule in a RuleList""" 0756 return english(name).replace(' ', '').replace('.', '') 0757 0758 0759 class Rule(RuleBase): 0760 0761 """a mahjongg rule with a name, matching variants, and resulting score. 0762 The rule applies if at least one of the variants matches the hand. 0763 For parameter rules, only use name, definition,parameter. definition must start with int or str 0764 which is there for loading&saving, but internally is stripped off.""" 0765 # pylint: disable=too-many-arguments,too-many-instance-attributes 0766 0767 ruleCode = {} 0768 limitHand = None 0769 0770 @classmethod 0771 def memoize(cls, func, srcClass): 0772 """cache results for func""" 0773 code = func.__code__ 0774 clsMethod = code.co_varnames[0] == 'cls' 0775 0776 def wrapper(*args): 0777 """closure""" 0778 hand = args[1] if clsMethod else args[0] 0779 cacheKey = (cls, func.__name__) 0780 if cacheKey not in hand.ruleCache: 0781 result = func(*args) 0782 hand.ruleCache[cacheKey] = result 0783 if Debug.ruleCache: 0784 hand.debug( 0785 'new ruleCache entry for hand %s: %s=%s' % 0786 (id(hand) % 0787 10000, cacheKey, result)) 0788 return result 0789 if Debug.ruleCache: 0790 if hand.ruleCache[cacheKey] != func(*args): 0791 hand.player.game.debug( 0792 'cacheKey=%s rule=%s func:%s args:%s' % 0793 (cacheKey, srcClass, func, args)) 0794 hand.player.game.debug( 0795 ' hand:%s/%s' % 0796 (id(hand), hand)) 0797 hand.player.game.debug( 0798 ' cached:%s ' % 0799 str(hand.ruleCache[cacheKey])) 0800 hand.player.game.debug( 0801 ' real:%s ' % 0802 str(func(*args))) 0803 return hand.ruleCache[cacheKey] 0804 return classmethod(wrapper) if clsMethod else staticmethod(wrapper) 0805 0806 def __init__(self, name, definition='', points=0, doubles=0, limits=0, 0807 description=None, explainTemplate=None, debug=False): 0808 RuleBase.__init__(self, name, definition, description) 0809 self.hasSelectable = False 0810 self.explainTemplate = explainTemplate 0811 self.score = Score(points, doubles, limits) 0812 self.parameter = 0 0813 self.debug = debug 0814 self.__parseDefinition() 0815 0816 @staticmethod 0817 def redirectTo(srcClass, destClass, memoize=False): 0818 """inject my static and class methods into destClass, 0819 converting methods to staticmethod/classmethod as needed""" 0820 # also for inherited methods 0821 classes = list(reversed(srcClass.__mro__[:-2])) 0822 combinedDict = dict(classes[0].__dict__) 0823 for ancestor in classes[1:]: 0824 combinedDict.update(ancestor.__dict__) 0825 for funcName, method in combinedDict.items(): 0826 if isinstance(method, (types.FunctionType, classmethod, staticmethod)): 0827 if hasattr(method, 'im_func'): 0828 method = method.__func__ 0829 elif hasattr(method, '__func__'): 0830 method = method.__func__ 0831 if memoize and method.__name__ in srcClass.cache: 0832 method = destClass.memoize(method, srcClass) 0833 else: 0834 if method.__code__.co_varnames[0] == 'cls': 0835 methodType = classmethod 0836 else: 0837 methodType = staticmethod 0838 method = methodType(method) 0839 setattr(destClass, funcName, method) 0840 0841 @classmethod 0842 def importRulecode(cls): 0843 """for every RuleCode class defined in this module, 0844 generate an instance and add it to dict Rule.ruleImpl. 0845 Also convert all RuleCode methods into classmethod or staticmethod""" 0846 if not cls.ruleCode: 0847 import rulecode 0848 for ruleClass in rulecode.__dict__.values(): 0849 if hasattr(ruleClass, "__mro__"): 0850 if ruleClass.__mro__[-2].__name__ == 'RuleCode' and len(ruleClass.__mro__) > 2: 0851 cls.ruleCode[ruleClass.__name__] = ruleClass 0852 # this changes all methods to classmethod or 0853 # staticmethod 0854 cls.redirectTo(ruleClass, ruleClass) 0855 0856 def key(self): 0857 """the key is used for finding a rule in a RuleList""" 0858 return ruleKey(self.name) 0859 0860 def __parseDefinition(self): 0861 """private setter for definition""" 0862 # pylint: disable=too-many-branches 0863 if not self.definition: 0864 return # may happen with special programmed rules 0865 variants = self.definition.split('||') 0866 self.__class__.options = {} 0867 self.hasSelectable = False 0868 for idx, variant in enumerate(variants): 0869 variant = str(variant) 0870 if variant[0] == 'F': 0871 assert idx == 0 0872 code = self.ruleCode[variant[1:]] 0873 # when executing code for this rule, we do not want 0874 # to call those things indirectly 0875 # pylint: disable=attribute-defined-outside-init 0876 self.redirectTo(code, self.__class__, memoize=True) 0877 if hasattr(code, 'selectable'): 0878 self.hasSelectable = True 0879 elif variant[0] == 'O': 0880 for action in variant[1:].split(): 0881 aParts = action.split('=') 0882 if len(aParts) == 1: 0883 aParts.append('None') 0884 self.options[aParts[0]] = aParts[1] 0885 else: 0886 pass 0887 self.validate() 0888 0889 def validate(self): 0890 """check for validity""" 0891 payers = int(self.options.get('payers', 1)) 0892 payees = int(self.options.get('payees', 1)) 0893 if not 2 <= payers + payees <= 4: 0894 logException( 0895 i18nc( 0896 '%1 can be a sentence', '%4 have impossible values %2/%3 in rule "%1"', 0897 self.name, payers, payees, 'payers/payees')) 0898 0899 def explain(self, meld): 0900 """use this rule for scoring""" 0901 return '%s: %s' % (i18n( 0902 self.explainTemplate if self.explainTemplate else self.name).format( 0903 group=meld[0].groupName() if meld else '', 0904 value=meld[0].valueName() if meld else '', 0905 meldType=meld.typeName() if meld else '', 0906 meldName=meld.name() if meld else '', 0907 tileName=meld[0].name() if meld else '').replace( 0908 '&', '').replace(' ', ' ').strip(), self.score.i18nStr()) 0909 0910 def hashStr(self): 0911 """ 0912 all that is needed to hash this rule. Try not to change this to keep 0913 database congestion low. 0914 0915 @return: The unique hash string 0916 @rtype: str 0917 """ 0918 return '%s: %s %s' % (self.name, self.definition, self.score) 0919 0920 def i18nStr(self): 0921 """return a human readable string with the content""" 0922 return self.score.i18nStr() 0923 0924 @staticmethod 0925 def exclusive(): 0926 """True if this rule can only apply to one player""" 0927 return False 0928 0929 def hasNonValueAction(self): 0930 """Rule has a special action not changing the score directly""" 0931 return bool(any(x not in ['lastsource', 'announcements'] for x in self.options)) 0932 0933 0934 class ParameterRule(RuleBase): 0935 0936 """for parameters""" 0937 prefix = 0 0938 0939 def __init__(self, name, definition, description, parameter): 0940 RuleBase.__init__(self, name, definition, description) 0941 defParts = definition.split('||') 0942 self.parName = defParts[0][len(self.prefix):] 0943 self.score = Score() 0944 self.parameter = self.convertParameter(parameter) 0945 0946 def key(self): 0947 """the key is used for finding a rule in a RuleList""" 0948 return self.parName 0949 0950 @staticmethod 0951 def convertParameter(parameter): 0952 """convert string to wanted type""" 0953 return parameter 0954 0955 def hashStr(self): 0956 """ 0957 all that is needed to hash this rule. Try not to change this to keep 0958 database congestion low. 0959 0960 @return: The unique hash string 0961 @rtype: str 0962 """ 0963 result = '%s: %s %s' % (self.name, self.definition, self.parameter) 0964 return result 0965 0966 def i18nStr(self): 0967 """return a human readable string with the content""" 0968 return str(self.parameter) 0969 0970 0971 class IntRule(ParameterRule): 0972 0973 """for int parameters. Duck typing with Rule""" 0974 prefix = 'int' 0975 0976 def __init__(self, name, definition, description, parameter): 0977 ParameterRule.__init__(self, name, definition, description, parameter) 0978 self.minimum = 0 0979 for defPart in definition.split('||'): 0980 if defPart.startswith('Omin='): 0981 self.minimum = int(defPart[5:]) 0982 0983 @staticmethod 0984 def convertParameter(parameter): 0985 """convert string to wanted type""" 0986 return int(parameter) 0987 0988 def validate(self): 0989 """is the rule valid?""" 0990 if self.parameter < self.minimum: 0991 return i18nc( 0992 'wrong value for rule', '%1: %2 is too small, minimal value is %3', 0993 i18n(self.name), self.parName, self.minimum) 0994 return None 0995 0996 0997 class BoolRule(ParameterRule): 0998 0999 """for bool parameters. Duck typing with Rule""" 1000 prefix = 'bool' 1001 1002 def __init__(self, name, definition, description, parameter): 1003 ParameterRule.__init__(self, name, definition, description, parameter) 1004 1005 @staticmethod 1006 def convertParameter(parameter): 1007 """convert string to wanted type""" 1008 return parameter not in ('false', 'False', False, 0, '0', None, '') 1009 1010 1011 class StrRule(ParameterRule): 1012 1013 """for str parameters. Duck typing with Rule. Currently not used.""" 1014 prefix = 'str' 1015 1016 def __init__(self, name, definition, description, parameter): 1017 ParameterRule.__init__(self, name, definition, description, parameter) 1018 1019 1020 class PredefinedRuleset(Ruleset): 1021 1022 """special code for loading rules from program code instead of from the database""" 1023 1024 classes = set() # only those will be playable 1025 preRulesets = [] 1026 1027 def __init__(self, name=None): 1028 Ruleset.__init__(self, name or 'general predefined ruleset') 1029 1030 @staticmethod 1031 def rulesets(): 1032 """a list of instances for all predefined rulesets""" 1033 if not PredefinedRuleset.preRulesets: 1034 PredefinedRuleset.preRulesets = [ 1035 x() for x in sorted(PredefinedRuleset.classes, key=lambda x: x.__name__)] 1036 return PredefinedRuleset.preRulesets 1037 1038 def rules(self): 1039 """here the predefined rulesets can define their rules""" 1040 1041 def clone(self): 1042 """return a clone, unloaded""" 1043 return self.__class__()