This commit is contained in:
2026-03-18 15:33:24 +01:00
parent 5e7a6808ab
commit 867c51062b
39 changed files with 6499 additions and 161 deletions

View File

@@ -6,6 +6,9 @@ from urllib.parse import quote
from datetime import datetime, timedelta
from time import sleep
from config import WIKIRANK_USER_AGENT
HEADERS = {"User-Agent": WIKIRANK_USER_AGENT}
logger = logging.getLogger("app")
class CardType(Enum):
@@ -14,11 +17,11 @@ class CardType(Enum):
location = 2
artwork = 3
life_form = 4
conflict = 5
event = 5
group = 6
science_thing = 7
vehicle = 8
business = 9
organization = 9
class CardRarity(Enum):
common = 0
@@ -52,9 +55,11 @@ class Card(NamedTuple):
rarity_text = f"Rarity: {self.card_rarity.name}"
return_string += ""+f"{rarity_text:<50}"+"\n"
return_string += ""+""*50+"\n"
link = "Image: "+("Yes" if self.image_link != "" else "No")
link = "Image: "+self.image_link
return_string += ""+f"{link:{' '}<50}"+"\n"
return_string += ""+""*50+"\n"
return_string += ""+f"{'*'*self.cost:{' '}<50}"+"\n"
return_string += ""+""*50+"\n"
lines = []
words = self.text.split(" ")
current_line = ""
@@ -85,14 +90,16 @@ class Card(NamedTuple):
WIKIDATA_INSTANCE_TYPE_MAP = {
"Q5": CardType.person, # human
"Q15632617": CardType.person, # fictional human
"Q215627": CardType.person, # person
"Q15632617": CardType.person, # fictional human
"Q22988604": CardType.person, # fictional human
"Q7889": CardType.artwork, # video game
"Q1004": CardType.artwork, # comics
"Q15416": CardType.artwork, # television program
"Q7889": CardType.artwork, # video game
"Q11424": CardType.artwork, # film
"Q24862": CardType.artwork, # film
"Q11410": CardType.artwork, # game
"Q15416": CardType.artwork, # television program
"Q24862": CardType.artwork, # short film
"Q11032": CardType.artwork, # newspaper
"Q25379": CardType.artwork, # play
"Q41298": CardType.artwork, # magazine
@@ -101,15 +108,18 @@ WIKIDATA_INSTANCE_TYPE_MAP = {
"Q169930": CardType.artwork, # EP
"Q196600": CardType.artwork, # media franchise
"Q202866": CardType.artwork, # animated film
"Q277759": CardType.artwork, # book series
"Q734698": CardType.artwork, # collectible card game
"Q506240": CardType.artwork, # television film
"Q738377": CardType.artwork, # student newspaper
"Q1259759": CardType.artwork, # miniseries
"Q3305213": CardType.artwork, # painting
"Q3177859": CardType.artwork, # dedicated deck card game
"Q5398426": CardType.artwork, # television series
"Q7725634": CardType.artwork, # literary work
"Q1761818": CardType.artwork, # advertising campaign
"Q1446621": CardType.artwork, # recital
"Q1868552": CardType.artwork, # local newspaper
"Q63952888": CardType.artwork, # anime television series
"Q47461344": CardType.artwork, # written work
"Q71631512": CardType.artwork, # tabletop role-playing game supplement
@@ -119,30 +129,53 @@ WIKIDATA_INSTANCE_TYPE_MAP = {
"Q105543609": CardType.artwork, # musical work / composition
"Q106499608": CardType.artwork, # literary reading
"Q117467246": CardType.artwork, # animated television series
"Q106042566": CardType.artwork, # single album
"Q515": CardType.location, # city
"Q8502": CardType.location, # mountain
"Q4022": CardType.location, # river
"Q6256": CardType.location, # country
"Q15284": CardType.location, # municipality
"Q27686": CardType.location, # hotel
"Q41176": CardType.location, # building
"Q23442": CardType.location, # island
"Q82794": CardType.location, # geographic region
"Q34442": CardType.location, # road
"Q398141": CardType.location, # school district
"Q133056": CardType.location, # mountain pass
"Q3624078": CardType.location, # sovereign state
"Q1093829": CardType.location, # city in the United States
"Q7930989": CardType.location, # city/town
"Q1250464": CardType.location, # realm
"Q3146899": CardType.location, # diocese of the catholic church
"Q35145263": CardType.location, # natural geographic object
"Q16521": CardType.life_form, # taxon
"Q310890": CardType.life_form, # monotypic taxon
"Q23038290": CardType.life_form, # fossil taxon
"Q12045585": CardType.life_form, # cattle breed
"Q198": CardType.conflict, # war
"Q8465": CardType.conflict, # civil war
"Q103495": CardType.conflict, # world war
"Q997267": CardType.conflict, # skirmish
"Q178561": CardType.conflict, # battle
"Q1361229": CardType.conflict, # conquest
"Q198": CardType.event, # war
"Q8465": CardType.event, # civil war
"Q141022": CardType.event, # eclipse
"Q103495": CardType.event, # world war
"Q350604": CardType.event, # armed conflict
"Q217327": CardType.event, # suicide attack
"Q750215": CardType.event, # mass murder
"Q898712": CardType.event, # aircraft hijacking
"Q997267": CardType.event, # skirmish
"Q178561": CardType.event, # battle
"Q273120": CardType.event, # protest
"Q1190554": CardType.event, # occurrence
"Q1361229": CardType.event, # conquest
"Q2223653": CardType.event, # terrorist attack
"Q2672648": CardType.event, # social conflict
"Q16510064": CardType.event, # sporting event
"Q10688145": CardType.event, # season
"Q13418847": CardType.event, # historical event
"Q13406554": CardType.event, # sports competition
"Q15275719": CardType.event, # recurring event
"Q114609228": CardType.event, # recurring sporting event
"Q7278": CardType.group, # political party
"Q476028": CardType.group, # association football club
@@ -150,25 +183,43 @@ WIKIDATA_INSTANCE_TYPE_MAP = {
"Q215380": CardType.group, # musical group
"Q176799": CardType.group, # military unit
"Q178790": CardType.group, # labor union
"Q851990": CardType.group, # people group
"Q2367225": CardType.group, # university and college sports club
"Q4801149": CardType.group, # artillery brigade
"Q9248092": CardType.group, # infantry division
"Q7210356": CardType.group, # political organization
"Q5419137": CardType.group, # veterans' organization
"Q6979593": CardType.group, # national association football team
"Q1194951": CardType.group, # national sports team
"Q1539532": CardType.group, # sports season of a sports club
"Q13393265": CardType.group, # basketball team
"Q17148672": CardType.group, # social club
"Q12973014": CardType.group, # sports team
"Q11446438": CardType.group, # female idol group
"Q10517054": CardType.group, # handball team
"Q135408445": CardType.group, # men's national association football team
"Q120143756": CardType.group, # division
"Q134601727": CardType.group, # group of persons
"Q127334927": CardType.group, # band
"Q523": CardType.science_thing, # star
"Q318": CardType.science_thing, # galaxy
"Q6999": CardType.science_thing, # astronomical object
"Q7187": CardType.science_thing, # gene
"Q8054": CardType.science_thing, # protein
"Q12136": CardType.science_thing, # disease
"Q65943": CardType.science_thing, # theorem
"Q12140": CardType.science_thing, # medication
"Q11276": CardType.science_thing, # globular cluster
"Q83373": CardType.science_thing, # quasar
"Q898273": CardType.science_thing, # protein domain
"Q134808": CardType.science_thing, # vaccine
"Q168845": CardType.science_thing, # star cluster
"Q1491746": CardType.science_thing, # galaxy group
"Q1341811": CardType.science_thing, # astronomical maser
"Q1840368": CardType.science_thing, # cloud type
"Q2154519": CardType.science_thing, # astrophysical x-ray source
"Q17444909": CardType.science_thing, # astronomical object type
"Q113145171": CardType.science_thing, # type of chemical entity
"Q1420": CardType.vehicle, # car
@@ -176,6 +227,11 @@ WIKIDATA_INSTANCE_TYPE_MAP = {
"Q43193": CardType.vehicle, # truck
"Q25956": CardType.vehicle, # space station
"Q39804": CardType.vehicle, # cruise ship
"Q170483": CardType.vehicle, # sailing ship
"Q964162": CardType.vehicle, # express train
"Q848944": CardType.vehicle, # merchant vessel
"Q189418": CardType.vehicle, # brigantine
"Q281019": CardType.vehicle, # ghost ship
"Q811704": CardType.vehicle, # rolling stock class
"Q673687": CardType.vehicle, # racing automobile
"Q174736": CardType.vehicle, # destroyer
@@ -184,21 +240,28 @@ WIKIDATA_INSTANCE_TYPE_MAP = {
"Q830335": CardType.vehicle, # protected cruiser
"Q928235": CardType.vehicle, # sloop-of-war
"Q391022": CardType.vehicle, # research vessel
"Q202527": CardType.vehicle, # minesweeper
"Q1185562": CardType.vehicle, # light aircraft carrier
"Q7233751": CardType.vehicle, # post ship
"Q3231690": CardType.vehicle, # automobile model
"Q1428357": CardType.vehicle, # submarine class
"Q1499623": CardType.vehicle, # destroyer escort
"Q4818021": CardType.vehicle, # attack submarine
"Q19832486": CardType.vehicle, # locomotive class
"Q23866334": CardType.vehicle, # motorcycle model
"Q29048322": CardType.vehicle, # vehicle model
"Q137188246": CardType.vehicle, # combat vehicle model
"Q100709275": CardType.vehicle, # combat vehicle family
"Q4830453": CardType.business, # business
"Q43229": CardType.organization, # organization
"Q47913": CardType.organization, # intelligence agency
"Q35535": CardType.organization, # police
"Q4830453": CardType.organization, # business
}
import asyncio
import httpx
HEADERS = {"User-Agent": "WikiTCG/1.0 (nikolaj@gade.gg)"}
async def _get_random_summary_async(client: httpx.AsyncClient) -> dict:
try:
response = await client.get(
@@ -257,6 +320,24 @@ async def _get_page_summary_async(client: httpx.AsyncClient, title: str) -> dict
return response.json()
async def _get_superclasses(client: httpx.AsyncClient, qid: str) -> list[str]:
try:
response = await client.get(
"https://www.wikidata.org/wiki/Special:EntityData/" + qid + ".json",
headers=HEADERS
)
except:
return []
if not response.is_success:
return []
entity = response.json().get("entities", {}).get(qid, {})
subclass_claims = entity.get("claims", {}).get("P279", [])
return [
c.get("mainsnak", {}).get("datavalue", {}).get("value", {}).get("id")
for c in subclass_claims
if c.get("mainsnak", {}).get("datavalue")
]
async def _infer_card_type_async(client: httpx.AsyncClient, entity_id: str) -> tuple[CardType, str, int]:
try:
response = await client.get(
@@ -272,7 +353,11 @@ async def _infer_card_type_async(client: httpx.AsyncClient, entity_id: str) -> t
entity = response.json().get("entities", {}).get(entity_id, {})
claims = entity.get("claims", {})
instance_of_claims = claims.get("P31", [])
qids = [c.get("mainsnak", {}).get("datavalue", {}).get("value", {}).get("id") for c in instance_of_claims]
qids = [
c.get("mainsnak", {}).get("datavalue", {}).get("value", {}).get("id")
for c in instance_of_claims
if c.get("mainsnak", {}).get("datavalue")
]
sitelinks = entity.get("sitelinks", {})
language_count = sum(
@@ -281,12 +366,31 @@ async def _infer_card_type_async(client: httpx.AsyncClient, entity_id: str) -> t
and key not in ("commonswiki", "wikidatawiki", "specieswiki")
)
if "P625" in claims:
return CardType.location, (qids[0] if qids != [] else ""), language_count
# First pass: direct match
for qid in qids:
if qid in WIKIDATA_INSTANCE_TYPE_MAP:
return WIKIDATA_INSTANCE_TYPE_MAP[qid], qid, language_count
# Second pass: check superclasses of each instance-of QID
superclass_results = sum(
await asyncio.gather(*[_get_superclasses(client, qid) for qid in qids if qid]),
[])
for superclass_qid in superclass_results:
if superclass_qid in WIKIDATA_INSTANCE_TYPE_MAP:
return WIKIDATA_INSTANCE_TYPE_MAP[superclass_qid], superclass_qid, language_count
# Third pass: check superclasses of each superclass
superclass_results2 = sum(
await asyncio.gather(*[_get_superclasses(client, qid) for qid in superclass_results if qid])
,[])
for superclass_qid2 in superclass_results2:
if superclass_qid2 in WIKIDATA_INSTANCE_TYPE_MAP:
return WIKIDATA_INSTANCE_TYPE_MAP[superclass_qid2], superclass_qid2, language_count
# Fallback: coordinate location
if "P625" in claims:
return CardType.location, (qids[0] if qids else ""), language_count
return CardType.other, (qids[0] if qids != [] else ""), language_count
async def _get_wikirank_score(client: httpx.AsyncClient, title: str) -> float | None:
@@ -348,7 +452,7 @@ async def _get_monthly_pageviews(client: httpx.AsyncClient, title: str) -> int |
return None
items = response.json().get("items", [])
return items[0]["views"] if items else None
except Exception:
except:
return None
def _pageviews_to_defense(views: int | None) -> int:
@@ -378,6 +482,24 @@ async def _get_card_async(client: httpx.AsyncClient, page_title: str|None = None
(card_type, instance, language_count), score, views = await asyncio.gather(
card_type_task, wikirank_task, pageviews_task
)
if (
(card_type == CardType.other and instance == "") or
language_count == 0 or
score is None or
views is None
):
error_message = f"Could not generate card '{title}': "
if card_type == CardType.other and instance == "":
error_message += "Not instance of a class"
elif language_count == 0:
error_message += "No language pages found"
elif score is None:
error_message += "No wikirank score"
elif views is None:
error_message += "No monthly view data"
logger.warning(error_message)
return None
rarity = _score_to_rarity(score)
multiplier = RARITY_MULTIPLIER[rarity]
@@ -415,6 +537,29 @@ def generate_cards(size: int) -> list[Card]:
def generate_card(title: str) -> Card|None:
return asyncio.run(_get_specific_card_async(title))
# Cards helper function
def compute_deck_type(cards: list) -> str | None:
if len(cards) < 20:
return None
avg_atk = sum(c.attack for c in cards) / len(cards)
avg_def = sum(c.defense for c in cards) / len(cards)
avg_cost = sum(c.cost for c in cards) / len(cards)
if all(c.cost > 6 for c in cards):
return "Unplayable"
if sum(1 for c in cards if c.cost >= 10) == 1:
return "God Card"
if sum(1 for c in cards if c.cost >= 10) > 1:
return "Pantheon"
if avg_cost >= 3.2:
return "Control"
if avg_cost <= 1.8:
return "Rush"
if avg_def > avg_atk * 1.5:
return "Wall"
if avg_atk > avg_def * 1.5:
return "Aggro"
return "Balanced"
# for card in generate_cards(5):
# print(card)
@@ -436,4 +581,11 @@ def generate_card(title: str) -> Card|None:
# for card in generate_cards(100):
# if card.card_type == CardType.other:
# print(card)
# print(card)
# print(generate_card("9/11"))
# print(generate_card("Julius Caesar"))
# print(generate_card("List of lists of lists"))
# print(generate_card("Boudica"))
# print(generate_card("Harald Bluetooth"))
# print(generate_card("Nørrebro"))