* Code refactoring for card db change (#86) This assumes the following file structure: ``` card_db/ cards_db.json sets_db.json <language>/ cards_text.json sets_text.json ``` Files that are needed are: - cards_db.json - cards_text.json and sets_text.json for each supported language **Other changes:** Added option called `--edition` that can be "1", "2", "latest", or "all". Defaults to "all". This allows for a quick filtering for those that don't want everything (i.e., "all"). - "1" is for all 1st editions of expansions. It also includes 2nd edition update packs. - "2" is for all 2nd editions of expansions. So base, Dominion 2nd ed., and Intrigue 2nd ed. (no update packs, since already in 2nd edition) - "latest" is for base, Dominion 2nd ed., and Intrigue 2nd ed., and all the remaining 1st editions. Cards can be grouped 3 ways: - No grouping (default) - In expansion grouping invoked with `--special_card_groups`. - Grouping across expansions with `--exclude_events` and `--exclude_landmarks`. These groups are placed in a set called "Extras". `--exclude_prizes` is not currently implemented. Added an option called `--upgrade_with_expansion` which will put the upgraded cards into the corresponding earlier expansion. So all 1st edition cards as well as the upgrade cards appear in the 1st edition set. That way the cards are listed on the expansion dividers and any tab/ordering fit the expansion as a whole. And because of the deleted cards in 2nd edition, this is a different list of cards than just using the 2nd edition. * update set image mapping for 2nd edition icons * add improved set icons from https://boardgamegeek.com/filepage/73273/dominion-card-icons-vector-images * recompress all images * new format for cards_db.json and translations * Added short name to sets * Updates to allow blank set images for base cards and fix blank set image for the "Extras" set. Also removed base_set.png which is no longer needed (again!) * scaled all set images to be exactly 300 x 300 pixels for consistency and better printing * Updated __init__.py and cards.py to automatically find the "lowest cost" value from all the cards in a group. * Update carddb_tests.py * Updated set information in testcases * Changed test of cardset to test cardset_tag instead. Since that is what everything keys off of now. * Updated the language tests to pull in the language parts before making the check. * Standardize on ISO8859-15 (#98) * Remove Trash Card from 2nd edition, replace HTML line breaks & unicode * Better Error Handling for Font Errors * Added text formating codes Added text formatting codes for "extra" and "description" fields. This includes: <tab> and <t> to add a tab (4 spaces) <n> as an alternative to \n (hard new line) <br> as an alternative to <br /> (soft new line) <c> and <center> to center text in this paragraph until the next hard new line <l> and <left> to left align text in this paragraph until the next hard new line <r> and <right> to right align text in this paragraph until the next hard new line. <line> to add a hard new line, a centered dividing line, and a trailing hard new line. <line> to put a centered line This goes with the <b>..</b> for bold, <i>..</i> for italics, and <u>..</u> for underline that was already existing (but probably not remembered. * Update card count presentation (#116) * wvoigt added automatic bonus highlighting (#119)
239 lines
7.5 KiB
Python
239 lines
7.5 KiB
Python
import json
|
|
import os
|
|
import re
|
|
from reportlab.lib.units import cm
|
|
|
|
|
|
class Card(object):
|
|
|
|
sets = None
|
|
types = None
|
|
type_names = None
|
|
bonus_regex = None
|
|
|
|
class CardJSONEncoder(json.JSONEncoder):
|
|
|
|
def default(self, obj):
|
|
if isinstance(obj, Card):
|
|
return obj.__dict__
|
|
return json.JSONEncoder.default(self, obj)
|
|
|
|
@staticmethod
|
|
def decode_json(obj):
|
|
return Card(**obj)
|
|
|
|
def __init__(self, name=None, cardset='', types=None, cost='', description='',
|
|
potcost=0, debtcost=0, extra='', count=-1, card_tag='missing card_tag',
|
|
cardset_tags=None, group_tag='', group_top=False, image=None,
|
|
text_icon=None, cardset_tag=''):
|
|
|
|
if types is None:
|
|
types = [] # make sure types is a list
|
|
if cardset_tags is None:
|
|
cardset_tags = [] # make sure cardset_tags is a list
|
|
if name is None:
|
|
name = card_tag # make sure there is a meaningful default name
|
|
|
|
self.name = name.strip()
|
|
self.cardset = cardset.strip()
|
|
self.types = types
|
|
self.types_name = ""
|
|
self.cost = cost
|
|
self.description = description
|
|
self.potcost = potcost
|
|
self.debtcost = debtcost
|
|
self.extra = extra
|
|
self.card_tag = card_tag
|
|
self.cardset_tags = cardset_tags
|
|
self.group_tag = group_tag
|
|
self.group_top = group_top
|
|
self.image = image
|
|
self.text_icon = text_icon
|
|
self.cardset_tag = cardset_tag
|
|
if count < 0:
|
|
self.count = [self.getType().getTypeDefaultCardCount()]
|
|
elif count == 0:
|
|
self.count = []
|
|
else:
|
|
self.count = [int(count)]
|
|
|
|
def getCardCount(self):
|
|
return sum(i for i in self.count)
|
|
|
|
def setCardCount(self, value):
|
|
self.count = value
|
|
|
|
def addCardCount(self, value):
|
|
self.count.extend(value)
|
|
|
|
def getStackHeight(self, thickness):
|
|
# return height of the stacked cards in cm. Using hight in cm of a stack of 60 Copper cards as thickness.
|
|
return self.getCardCount() * cm * (thickness / 60.0) + 2
|
|
|
|
def getType(self):
|
|
return Card.types[tuple(self.types)]
|
|
|
|
def getBonusBoldText(self, text):
|
|
for regex in Card.bonus_regex:
|
|
text = re.sub(regex, '<b>\\1</b>', text)
|
|
return text
|
|
|
|
@staticmethod
|
|
def addBonusRegex(bonus):
|
|
# Each bonus_regex matches the bonus keywords to be highlighted
|
|
# This only needs to be done once per language
|
|
if Card.bonus_regex is None:
|
|
# initialize the information holder
|
|
Card.bonus_regex = []
|
|
|
|
# Make sure have minimum to to anything
|
|
if not isinstance(bonus, dict):
|
|
return
|
|
if 'include' not in bonus:
|
|
return
|
|
if not bonus['include']:
|
|
return
|
|
if 'exclude' not in bonus:
|
|
bonus['exclude'] = []
|
|
|
|
# Start processing of lists into a single regex statement
|
|
# (?i) makes this case insensitive
|
|
# (?!\<b\>) and (?!\<\/b\>) prevents matching already bolded items
|
|
# (?!\w) prevents smaller word matches. Prevents matching "Action" in "Actions"
|
|
if bonus['exclude']:
|
|
bonus['exclude'].sort(reverse=True)
|
|
exclude_regex = '(?!\w)(?!\s*(' + '|'.join(bonus['exclude']) + '))'
|
|
else:
|
|
exclude_regex = ''
|
|
|
|
bonus['include'].sort(reverse=True)
|
|
include_regex = "(\+\s*\d+\s*(" + '|'.join(bonus['include']) + "))"
|
|
regex = "((?i)(?!\<b\>)" + include_regex + exclude_regex + "(?!\<\/b\>))"
|
|
Card.bonus_regex.append(regex)
|
|
|
|
def __repr__(self):
|
|
return '"' + self.name + '"'
|
|
|
|
def toString(self):
|
|
return self.name + ' ' + self.cardset + ' ' + '-'.join(self.types)\
|
|
+ ' ' + self.cost + ' ' + self.description + ' ' + self.extra
|
|
|
|
def isType(self, what):
|
|
return what in self.getType().getTypeNames()
|
|
|
|
def isExpansion(self):
|
|
return self.isType('Expansion')
|
|
|
|
def isEvent(self):
|
|
return self.isType('Event')
|
|
|
|
def isLandmark(self):
|
|
return self.isType('Landmark')
|
|
|
|
def isPrize(self):
|
|
return self.isType('Prize')
|
|
|
|
def get_total_cost(self, c):
|
|
# Return a tuple that represents the total cost of card c
|
|
# Hightest cost cards are in order:
|
|
# - Landmarks to sort at the very end
|
|
# - cards with * since that can mean anything
|
|
# - cards with numeric errors
|
|
# convert cost (a string) into a number
|
|
if c.isLandmark():
|
|
c_cost = 999
|
|
elif not c.cost:
|
|
c_cost = 0 # if no cost, treat as 0
|
|
elif '*' in c.cost:
|
|
c_cost = 998 # make it a really big number
|
|
else:
|
|
try:
|
|
c_cost = int(c.cost)
|
|
except ValueError:
|
|
c_cost = 997 # can't, so make it a really big number
|
|
|
|
return c_cost, c.potcost, c.debtcost
|
|
|
|
def set_lowest_cost(self, other):
|
|
# set self cost fields to the lower of the two's total cost
|
|
self_cost = self.get_total_cost(self)
|
|
other_cost = self.get_total_cost(other)
|
|
if other_cost < self_cost:
|
|
self.cost = other.cost
|
|
self.potcost = other.potcost
|
|
self.debtcost = other.debtcost
|
|
|
|
def setImage(self):
|
|
setImage = None
|
|
if self.image is not None:
|
|
setImage = self.image
|
|
else:
|
|
if self.cardset_tag in Card.sets:
|
|
if 'image' in Card.sets[self.cardset_tag]:
|
|
setImage = Card.sets[self.cardset_tag]['image']
|
|
|
|
if setImage is None and self.cardset_tag != 'base':
|
|
print 'warning, no set image for set "{}", card "{}"'.format(self.cardset, self.name)
|
|
return setImage
|
|
|
|
def setTextIcon(self):
|
|
setTextIcon = None
|
|
if self.text_icon:
|
|
setTextIcon = self.text_icon
|
|
else:
|
|
if self.cardset_tag in Card.sets:
|
|
if 'text_icon' in Card.sets[self.cardset_tag]:
|
|
setTextIcon = Card.sets[self.cardset_tag]['text_icon']
|
|
|
|
if setTextIcon is None and self.cardset != 'base':
|
|
print 'warning, no set text for set "{}", card "{}"'.format(self.cardset, self.name)
|
|
return setTextIcon
|
|
|
|
def isBlank(self):
|
|
return self.isType('Blank')
|
|
|
|
|
|
class BlankCard(Card):
|
|
|
|
def __init__(self, num):
|
|
Card.__init__(self, str(num), 'extra', ('Blank',), 0)
|
|
|
|
def isBlank(self):
|
|
return True
|
|
|
|
|
|
class CardType(object):
|
|
|
|
@staticmethod
|
|
def decode_json(obj):
|
|
return CardType(**obj)
|
|
|
|
def __init__(self, card_type, card_type_image, defaultCardCount=10, tabTextHeightOffset=0, tabCostHeightOffset=-1):
|
|
self.typeNames = tuple(card_type)
|
|
self.tabImageFile = card_type_image
|
|
self.defaultCardCount = defaultCardCount
|
|
self.tabTextHeightOffset = tabTextHeightOffset
|
|
self.tabCostHeightOffset = tabCostHeightOffset
|
|
|
|
def getTypeDefaultCardCount(self):
|
|
return self.defaultCardCount
|
|
|
|
def getTypeNames(self):
|
|
return self.typeNames
|
|
|
|
def getTabImageFile(self):
|
|
if not self.tabImageFile:
|
|
return None
|
|
return self.tabImageFile
|
|
|
|
def getNoCoinTabImageFile(self):
|
|
if not self.tabImageFile:
|
|
return None
|
|
return ''.join(os.path.splitext(self.tabImageFile)[0] + '_nc' + os.path.splitext(self.tabImageFile)[1])
|
|
|
|
def getTabTextHeightOffset(self):
|
|
return self.tabTextHeightOffset
|
|
|
|
def getTabCostHeightOffset(self):
|
|
return self.tabCostHeightOffset
|