Coverage for thb/cards/base.py : 45%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2from __future__ import annotations
4# -- stdlib --
5from collections import deque
6from typing import Any, ClassVar, Dict, Iterable, List, Optional, Sequence, TYPE_CHECKING, Tuple
7from typing import Type
8from weakref import WeakValueDictionary
9import itertools
10import logging
11import random
13# -- third party --
14# -- own --
15from game.base import GameError, GameObject, GameViralContext, get_seed_for, Nobody
16from thb.mode import THBattle
18# -- typing --
19if TYPE_CHECKING:
20 from thb.actions import UserAction # noqa: F401
21 from thb.characters.base import Character # noqa: F401
22 from thb.meta.typing import CardMeta, SkillMeta # noqa: F401
25# -- code --
26log = logging.getLogger('THBattle_Cards')
27alloc_id = itertools.count(1).__next__
30class Card(GameObject, GameViralContext):
31 NOTSET = 0
32 SPADE = 1
33 HEART = 2
34 CLUB = 3
35 DIAMOND = 4
37 RED = 5
38 BLACK = 6
40 SUIT_REV = {
41 0: '?',
42 1: 'SPADE', 2: 'HEART',
43 3: 'CLUB', 4: 'DIAMOND',
44 }
46 NUM_REV = {
47 0: '?', 1: 'A', 2: '2', 3: '3', 4: '4',
48 5: '5', 6: '6', 7: '7', 8: '8', 9: '9',
49 10: '10', 11: 'J', 12: 'Q', 13: 'K',
50 }
52 _color: Optional[int] = None
53 usage = 'launch'
55 ui_meta: ClassVar[CardMeta]
57 associated_action: Optional[Type[UserAction]]
58 category: Sequence[str]
60 # True means this card's associated cards have already been taken.
61 # Only meaningful for virtual cards.
62 unwrapped = False
64 def __init__(self, suit=NOTSET, number=0, resides_in=None, track_id=0):
65 self.sync_id = 0 # Synchronization id, changes during shuffling, kept sync between client and server.
66 self.track_id = track_id # Card identifier, unique among all cards, 0 if doesn't care, or not known.
67 self.suit = suit
68 self.number = number
69 self.resides_in = resides_in
71 def dump(self):
72 return dict(
73 type=self.__class__.__name__,
74 suit=self.suit,
75 number=self.number,
76 sync_id=self.sync_id,
77 track_id=self.track_id,
78 )
80 def sync(self, data): # this only executes at client side, let it crash.
81 if data['sync_id'] != self.sync_id:
82 logging.error(
83 'CardOOS: server: %s, %s, %s, sync_id=%d; client: %s, %s, %s, sync_id=%d',
85 data['type'],
86 self.SUIT_REV.get(data['suit'], data['suit']),
87 self.NUM_REV.get(data['number'], data['number']),
88 data['sync_id'],
90 self.__class__.__name__,
91 self.SUIT_REV.get(self.suit),
92 self.NUM_REV.get(self.number),
93 self.sync_id,
94 )
95 raise GameError('Card: out of sync')
97 clsname = data['type']
98 cls = PhysicalCard.classes.get(clsname)
100 if not cls:
101 raise GameError('Card: unknown card class')
103 self.__class__ = cls
104 self.suit = data['suit']
105 self.number = data['number']
106 self.track_id = data['track_id']
108 @staticmethod
109 def copy(o):
110 return o.__class__(
111 o.suit,
112 o.number,
113 o.resides_in,
114 o.track_id,
115 )
117 def shred(self):
118 self.__class__ = ShreddedCard
119 self.suit = self.number = self.track_id = 0
121 def move_to(self, resides_in):
122 self.detach()
123 if resides_in is not None:
124 resides_in.append(self)
126 self.resides_in = resides_in
128 def detach(self):
129 try:
130 self.resides_in.remove(self)
131 except (AttributeError, ValueError):
132 pass
134 def attach(self):
135 if self not in self.resides_in:
136 self.resides_in.append(self)
138 @property
139 def detached(self):
140 return self.resides_in is not None and self not in self.resides_in
142 def __repr__(self):
143 return "{name}({suit}, {num}{detached}, SyncID:{sync_id})".format(
144 name=self.__class__.__name__,
145 suit=self.SUIT_REV.get(self.suit, self.suit),
146 num=self.NUM_REV.get(self.number, self.number),
147 detached=', detached' if self.detached else '',
148 sync_id=self.sync_id,
149 )
151 def is_card(self, cls):
152 return isinstance(self, cls)
154 @property
155 def color(self):
156 if self._color is not None: return self._color
157 s = self.suit
158 if s in (Card.HEART, Card.DIAMOND):
159 return Card.RED
160 elif s in (Card.SPADE, Card.CLUB):
161 return Card.BLACK
162 else:
163 return Card.NOTSET
165 @color.setter
166 def color(self, val):
167 self._color = val
169 def target(self: Any, src: Character, tl: Sequence[Character]) -> Tuple[List[Character], bool]:
170 raise Exception('Override this')
173class PhysicalCard(Card):
174 classes: ClassVar[Dict[str, Type[PhysicalCard]]] = {}
176 def __eq__(self, other):
177 if not isinstance(other, Card): return False
178 return self.sync_id == other.sync_id
180 def __ne__(self, other):
181 return not self.__eq__(other)
183 def __hash__(self):
184 return 84065234 + self.sync_id
187class VirtualCard(Card, GameViralContext):
188 associated_cards: Sequence[Card]
189 action_params: dict
190 no_reveal: ClassVar[bool] = False
191 game: THBattle
193 _suit: Optional[int]
194 _number: Optional[int]
195 _color: Optional[int]
197 def __init__(self, character: Character):
198 self.character = character
199 self.associated_cards = []
200 self.resides_in = character.cards
201 self.action_params = {}
202 self.unwrapped = False
203 self.sync_id = 0
204 self.usage = 'none'
205 self._suit = None
206 self._number = None
207 self._color = None
209 def dump(self):
210 return {
211 'class': self.__class__.__name__,
212 'sync_id': self.sync_id,
213 'vcard': True,
214 'params': self.action_params,
215 }
217 def check(self): # override this
218 return False
220 @classmethod
221 def unwrap(cls, vcards: Iterable[Card], include_vcards: bool = False) -> List[Card]:
222 lst: List[Card] = []
223 sl = list(vcards)
225 while sl:
226 s = sl.pop()
227 if isinstance(s, VirtualCard):
228 if include_vcards:
229 lst.append(s)
230 sl.extend(VirtualCard.unwrap(s.associated_cards, include_vcards))
231 else:
232 assert isinstance(s, (PhysicalCard, HiddenCard)), s
233 lst.append(s)
235 return lst
237 @classmethod
238 def wrap(cls, cl: List[Card], character: Character, params: Dict[str, Any] = None):
239 vc = cls(character)
240 vc.action_params = params or {}
241 vc.associated_cards = cl[:]
242 return vc
244 def get_color(self):
245 if self._color is not None:
246 return self._color
248 color = {c.color for c in self.associated_cards}
249 color = color.pop() if len(color) == 1 else Card.NOTSET
250 return color
252 def set_color(self, v):
253 self._color = v
255 color = property(get_color, set_color)
257 def get_number(self):
258 if self._number is not None:
259 return self._number
261 num = {c.number for c in self.associated_cards}
262 num = num.pop() if len(num) == 1 else Card.NOTSET
263 return num
265 def set_number(self, v):
266 self._number = v
268 number = property(get_number, set_number)
270 def get_suit(self) -> int:
271 if self._suit is not None:
272 return self._suit
274 cl = self.associated_cards
275 suit = cl[0].suit if len(cl) == 1 else Card.NOTSET
276 return suit
278 def set_suit(self, v: int):
279 self._suit = v
281 suit = property(get_suit, set_suit)
283 def sync(self, data):
284 assert data['vcard']
285 assert self.__class__.__name__ == data['class']
286 assert self.sync_id == data['sync_id']
287 assert self.action_params == data['params']
289 @staticmethod
290 def find_in_hierarchy(card, cls):
291 if card.is_card(cls):
292 return card
294 if not card.is_card(VirtualCard):
295 return None
297 for c in card.associated_cards:
298 r = VirtualCard.find_in_hierarchy(c, cls)
299 if r: return r
301 return None
304class CardList(GameObject, deque):
305 DECKCARD = 'deckcard'
306 DROPPEDCARD = 'droppedcard'
307 CARDS = 'cards'
308 SHOWNCARDS = 'showncards'
309 EQUIPS = 'equips'
310 FATETELL = 'fatetell'
311 SPECIAL = 'special'
313 def __init__(self, owner: Optional['Character'], typ: str):
314 self.owner = owner
315 self.type = typ
316 deque.__init__(self)
318 def __eq__(self, rhs):
319 # two empty card lists is not the same.
320 # card list never equals to a deque.
321 return self is rhs
323 def __repr__(self):
324 return "CardList(owner=%s, type=%s, len == %d)" % (self.owner, self.type, len(self))
327class Deck(GameObject):
328 def __init__(self, g: THBattle, card_definition=None):
329 from thb.cards import definition
330 self.game = g
331 card_definition = card_definition or definition.card_definition
333 self.cards_record: Dict[int, PhysicalCard] = {}
334 self.vcards_record: WeakValueDictionary[int, VirtualCard] = WeakValueDictionary()
335 self.droppedcards = CardList(None, 'droppedcard')
336 cards = CardList(None, 'deckcard')
337 self.cards = cards
338 cards.extend(
339 cls(suit, rank, cards, track_id=alloc_id())
340 for cls, suit, rank in card_definition
341 )
342 self.shuffle(cards)
344 def getcards(self, num: int) -> List[Card]:
345 cl = self.cards
346 if len(self.cards) <= num:
347 dcl = self.droppedcards
349 assert all(not c.is_card(VirtualCard) for c in dcl)
350 assert all(not c.is_card(ShreddedCard) for c in dcl)
352 dropped = list(dcl)
353 dcl.clear()
354 rest = list(cl)
355 cl.clear()
357 dcl.extend(dropped[-10:])
358 cl.extend([Card.copy(c) for c in dropped[:-10]])
359 self.shuffle(cl)
360 cl.extendleft(reversed(rest))
362 # assert all(not isinstance(c, ShreddedCard) for c in cl)
363 # assert rest == list(cl)[:len(rest)]
365 cl = self.cards
366 rst = []
367 for i in range(min(len(cl), num)):
368 rst.append(cl[i])
370 return rst
372 def lookup(self, sync_id: int) -> Optional[Card]:
373 return self.vcards_record.get(sync_id, None) or \
374 self.cards_record.get(sync_id, None)
376 def register_card(self, card: PhysicalCard):
377 assert not card.sync_id
378 g = self.game
379 sid = g.get_synctag()
380 card.sync_id = sid
381 self.cards_record[sid] = card
382 return sid
384 def register_vcard(self, vc: VirtualCard):
385 g = self.game
386 sid = g.get_synctag()
387 vc.sync_id = sid
388 self.vcards_record[sid] = vc
389 return sid
391 def shuffle(self, cl: CardList):
392 owner = cl.owner.player if cl.owner else Nobody()
394 g = self.game
396 # assert all(c.resides_in is cl for c in cl)
398 seed = get_seed_for(g, owner)
400 if seed: # cardlist owner & server 400 ↛ 405line 400 didn't jump to line 405, because the condition on line 400 was never false
401 cards = [Card.copy(c) for c in cl]
402 shuffler = random.Random(seed)
403 shuffler.shuffle(cards)
404 else: # others
405 cards = [HiddenCard() for c in cl]
407 for c in cl:
408 c.shred()
410 for c in cards:
411 c.resides_in = cl
413 cl.clear()
414 cl.extend(cards)
416 for c in cl:
417 self.register_card(c)
419 # assert all(not isinstance(c, ShreddedCard) for c in cl)
421 def inject(self, cls: Type[PhysicalCard], suit: int, rank: int) -> PhysicalCard:
422 cl = self.cards
423 c = cls(suit, rank, cl)
424 self.register_card(c)
425 cl.appendleft(c)
426 return c
429class Skill(VirtualCard):
430 category: Sequence[str] = ['skill']
431 skill_category: Sequence[str] = []
433 ui_meta: ClassVar[SkillMeta]
435 def __init__(self, character):
436 assert character is not None
437 VirtualCard.__init__(self, character)
438 self.usage = 'launch'
440 def check(self): # override this
441 return False
444class TreatAs(object):
445 treat_as: Type[PhysicalCard]
446 usage = 'launch'
448 if TYPE_CHECKING:
449 category: Sequence[str] = ['skill', 'treat_as']
450 else:
451 @property
452 def category(self) -> Sequence[str]:
453 return ['skill', 'treat_as'] + list(self.treat_as.category)
455 def check(self):
456 return False
458 def is_card(self, cls):
459 if cls is PhysicalCard:
460 return False
462 if issubclass(self.treat_as, cls):
463 return True
465 return isinstance(self, cls)
467 def target(self: Any, src: Character, tl: Sequence[Character]) -> Tuple[List[Character], bool]:
468 return self.treat_as().target(src, tl)
470 def __getattr__(self, name):
471 return getattr(self.treat_as, name)
474# card targets:
475def t_None():
476 def t_None(self, src: Character, tl: Sequence[Character]) -> Tuple[List[Character], bool]:
477 return ([], False)
478 return t_None
481def t_Self():
482 def t_Self(self, src: Character, tl: Sequence[Character]) -> Tuple[List[Character], bool]:
483 return ([src], True)
484 return t_Self
487def t_OtherOne():
488 def t_OtherOne(self, src: Character, tl: Sequence[Character]) -> Tuple[List[Character], bool]:
489 tl = [t for t in tl if not t.dead]
490 try:
491 tl.remove(src)
492 except ValueError:
493 pass
494 return (tl[-1:], bool(len(tl)))
495 return t_OtherOne
498def t_One():
499 def t_One(self, src: Character, tl: Sequence[Character]) -> Tuple[List[Character], bool]:
500 tl = [t for t in tl if not t.dead]
501 return (tl[-1:], bool(len(tl)))
502 return t_One
505def t_All():
506 def t_All(self, src: Character, tl: Sequence[Character]) -> Tuple[List[Character], bool]:
507 g = self.game
508 return ([t for t in g.players.rotate_to(src)[1:] if not t.dead], True)
509 return t_All
512def t_AllInclusive():
513 def t_AllInclusive(self, src: Character, tl: Sequence[Character]) -> Tuple[List[Character], bool]:
514 g = self.game
515 pl = g.players.rotate_to(src)
516 return ([t for t in pl if not t.dead], True)
517 return t_AllInclusive
520def t_OtherLessEqThanN(n):
521 def t_OtherLessEqThanN(self, src: Character, tl: Sequence[Character]) -> Tuple[List[Character], bool]:
522 tl = [t for t in tl if not t.dead]
523 try:
524 tl.remove(src)
525 except ValueError:
526 pass
527 return (tl[:n], bool(len(tl)))
529 t_OtherLessEqThanN._for_test_OtherLessEqThanN = n # type: ignore
530 return t_OtherLessEqThanN
533def t_OneOrNone():
534 def t_OneOrNone(self, src: Character, tl: Sequence[Character]) -> Tuple[List[Character], bool]:
535 tl = [t for t in tl if not t.dead]
536 return (tl[-1:], True)
537 return t_OneOrNone
540def t_OtherN(n):
541 def t_OtherN(self, src: Character, tl: Sequence[Character]) -> Tuple[List[Character], bool]:
542 tl = [t for t in tl if not t.dead]
543 try:
544 tl.remove(src)
545 except ValueError:
546 pass
547 return (tl[:n], bool(len(tl) >= n))
549 t_OtherN._for_test_OtherN = n # type: ignore
550 return t_OtherN
553class HiddenCard(Card): # special thing....
554 associated_action = None
555 target = t_None()
558class ShreddedCard(Card): # Become this after shuffling
559 associated_action = None
560 target = t_None()
563class DummyCard(PhysicalCard): # another special thing....
564 associated_action = None
565 target = t_None()
566 category = ['dummy']
568 def __init__(self, suit=Card.NOTSET, number=0, resides_in=None, **kwargs):
569 Card.__init__(self, suit, number, resides_in)
570 self.__dict__.update(kwargs)