Coverage for thb/cards/base.py : 86%
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
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: 81 ↛ 82line 81 didn't jump to line 82, because the condition on line 81 was never true
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: 100 ↛ 101line 100 didn't jump to line 101, because the condition on line 100 was never true
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: 123 ↛ 126line 123 didn't jump to line 126, because the condition on line 123 was never false
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 156 ↛ exitline 156 didn't return from function 'color', because the return on line 156 wasn't executed
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 exinwan_target: Optional[Character] # HACK, for ExinwanCard
178 def __eq__(self, other):
179 if not isinstance(other, Card): return False 179 ↛ exitline 179 didn't return from function '__eq__', because the return on line 179 wasn't executed
180 return self.sync_id == other.sync_id
182 def __ne__(self, other):
183 return not self.__eq__(other)
185 def __hash__(self):
186 return 84065234 + self.sync_id
189class VirtualCard(Card, GameViralContext):
190 associated_cards: Sequence[Card]
191 action_params: dict
192 no_reveal: ClassVar[bool] = False
194 _suit: Optional[int]
195 _number: Optional[int]
196 _color: Optional[int]
198 def __init__(self, character: Character):
199 self.character = character
200 self.associated_cards = []
201 self.resides_in = character.cards
202 self.action_params = {}
203 self.unwrapped = False
204 self.sync_id = 0
205 self.usage = 'none'
206 self._suit = None
207 self._number = None
208 self._color = None
210 def dump(self):
211 return {
212 'class': self.__class__.__name__,
213 'sync_id': self.sync_id,
214 'vcard': True,
215 'params': self.action_params,
216 }
218 def check(self): # override this
219 return False
221 @classmethod
222 def unwrap(cls, vcards: Iterable[Card], include_vcards: bool = False) -> List[Card]:
223 lst: List[Card] = []
224 sl = list(vcards)
226 while sl:
227 s = sl.pop()
228 if isinstance(s, VirtualCard):
229 if include_vcards:
230 lst.append(s)
231 sl.extend(VirtualCard.unwrap(s.associated_cards, include_vcards))
232 else:
233 assert isinstance(s, (PhysicalCard, HiddenCard)), s
234 lst.append(s)
236 return lst
238 @classmethod
239 def wrap(cls, cl: List[Card], character: Character, params: Dict[str, Any] = None):
240 vc = cls(character)
241 vc.action_params = params or {}
242 vc.associated_cards = cl[:]
243 return vc
245 def get_color(self):
246 if self._color is not None: 246 ↛ 247line 246 didn't jump to line 247, because the condition on line 246 was never true
247 return self._color
249 color = {c.color for c in self.associated_cards}
250 color = color.pop() if len(color) == 1 else Card.NOTSET
251 return color
253 def set_color(self, v):
254 self._color = v
256 color = property(get_color, set_color)
258 def get_number(self):
259 if self._number is not None:
260 return self._number
262 num = {c.number for c in self.associated_cards}
263 num = num.pop() if len(num) == 1 else Card.NOTSET
264 return num
266 def set_number(self, v):
267 self._number = v
269 number = property(get_number, set_number)
271 def get_suit(self) -> int:
272 if self._suit is not None: 272 ↛ 273line 272 didn't jump to line 273, because the condition on line 272 was never true
273 return self._suit
275 cl = self.associated_cards
276 suit = cl[0].suit if len(cl) == 1 else Card.NOTSET
277 return suit
279 def set_suit(self, v: int):
280 self._suit = v
282 suit = property(get_suit, set_suit)
284 def sync(self, data):
285 assert data['vcard']
286 assert self.__class__.__name__ == data['class']
287 assert self.sync_id == data['sync_id']
288 assert self.action_params == data['params']
290 @staticmethod
291 def find_in_hierarchy(card, cls):
292 if card.is_card(cls):
293 return card
295 if not card.is_card(VirtualCard):
296 return None
298 for c in card.associated_cards:
299 r = VirtualCard.find_in_hierarchy(c, cls)
300 if r: return r
302 return None
305class CardList(GameObject, deque):
306 DECKCARD = 'deckcard'
307 DROPPEDCARD = 'droppedcard'
308 CARDS = 'cards'
309 SHOWNCARDS = 'showncards'
310 EQUIPS = 'equips'
311 FATETELL = 'fatetell'
312 SPECIAL = 'special'
314 def __init__(self, owner: Optional['Character'], typ: str):
315 self.owner = owner
316 self.type = typ
317 deque.__init__(self)
319 def __eq__(self, rhs):
320 # two empty card lists is not the same.
321 # card list never equals to a deque.
322 return self is rhs
324 def __repr__(self):
325 return "CardList(owner=%s, type=%s, len == %d)" % (self.owner, self.type, len(self))
328class Deck(GameObject):
329 def __init__(self, g: THBattle, card_definition=None):
330 from thb.cards import definition
331 self.game = g
332 card_definition = card_definition or definition.card_definition
334 self.cards_record: Dict[int, PhysicalCard] = {}
335 self.vcards_record: Dict[int, VirtualCard] = WeakValueDictionary()
336 self.droppedcards = CardList(None, 'droppedcard')
337 cards = CardList(None, 'deckcard')
338 self.cards = cards
339 cards.extend(
340 cls(suit, rank, cards, track_id=alloc_id())
341 for cls, suit, rank in card_definition
342 )
343 self.shuffle(cards)
345 def getcards(self, num: int) -> List[Card]:
346 cl = self.cards
347 if len(self.cards) <= num:
348 dcl = self.droppedcards
350 assert all(not c.is_card(VirtualCard) for c in dcl)
351 assert all(not c.is_card(ShreddedCard) for c in dcl)
353 dropped = list(dcl)
354 dcl.clear()
355 rest = list(cl)
356 cl.clear()
358 dcl.extend(dropped[-10:])
359 cl.extend([Card.copy(c) for c in dropped[:-10]])
360 self.shuffle(cl)
361 cl.extendleft(reversed(rest))
363 # assert all(not isinstance(c, ShreddedCard) for c in cl)
364 # assert rest == list(cl)[:len(rest)]
366 cl = self.cards
367 rst = []
368 for i in range(min(len(cl), num)):
369 rst.append(cl[i])
371 return rst
373 def lookup(self, sync_id: int) -> Optional[Card]:
374 return self.vcards_record.get(sync_id, None) or \
375 self.cards_record.get(sync_id, None)
377 def register_card(self, card: PhysicalCard):
378 assert not card.sync_id
379 g = self.game
380 sid = g.get_synctag()
381 card.sync_id = sid
382 self.cards_record[sid] = card
383 return sid
385 def register_vcard(self, vc: VirtualCard):
386 g = self.game
387 sid = g.get_synctag()
388 vc.sync_id = sid
389 self.vcards_record[sid] = vc
390 return sid
392 def shuffle(self, cl: CardList):
393 owner = cl.owner
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
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
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
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(Card): # 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)