# Coverage for thb/cards/base.py : 45%

## 355 statements 184 run 171 missing 10 excluded 1 partial

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 -*-

2 from __future__ import annotations

3

4# -- stdlib --

5 from collections import deque

6 from typing import Any, ClassVar, Dict, Iterable, List, Optional, Sequence, TYPE_CHECKING, Tuple

7 from typing import Type

8 from weakref import WeakValueDictionary

9 import itertools

10 import logging

11 import random

12

13# -- third party --

14# -- own --

15 from game.base import GameError, GameObject, GameViralContext, get_seed_for, Nobody

16 from thb.mode import THBattle

17

18# -- typing --

19 if 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

23

24

25# -- code --

26 log = logging.getLogger('THBattle_Cards')

27 alloc_id = itertools.count(1).__next__

28

29

30 class Card(GameObject, GameViralContext):

31 NOTSET = 0

32 SPADE = 1

33 HEART = 2

34 CLUB = 3

35 DIAMOND = 4

36

37 RED = 5

38 BLACK = 6

39

40 SUIT_REV = {

41 0: '?',

42 1: 'SPADE', 2: 'HEART',

43 3: 'CLUB', 4: 'DIAMOND',

44 }

45

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 }

51

52 _color: Optional[int] = None

53 usage = 'launch'

54

55 ui_meta: ClassVar[CardMeta]

56

57 associated_action: Optional[Type[UserAction]]

58 category: Sequence[str]

59

60 # True means this card's associated cards have already been taken.

61 # Only meaningful for virtual cards.

62 unwrapped = False

63

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

70

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 )

79

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',

84

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'],

89

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')

96

97 clsname = data['type']

98 cls = PhysicalCard.classes.get(clsname)

99

100 if not cls:

101 raise GameError('Card: unknown card class')

102

103 self.__class__ = cls

104 self.suit = data['suit']

105 self.number = data['number']

106 self.track_id = data['track_id']

107

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 )

116

117 def shred(self):

118 self.__class__ = ShreddedCard

119 self.suit = self.number = self.track_id = 0

120

121 def move_to(self, resides_in):

122 self.detach()

123 if resides_in is not None:

124 resides_in.append(self)

125

126 self.resides_in = resides_in

127

128 def detach(self):

129 try:

130 self.resides_in.remove(self)

131 except (AttributeError, ValueError):

132 pass

133

134 def attach(self):

135 if self not in self.resides_in:

136 self.resides_in.append(self)

137

138 @property

139 def detached(self):

140 return self.resides_in is not None and self not in self.resides_in

141

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 )

150

151 def is_card(self, cls):

152 return isinstance(self, cls)

153

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

164

165 @color.setter

166 def color(self, val):

167 self._color = val

168

169 def target(self: Any, src: Character, tl: Sequence[Character]) -> Tuple[List[Character], bool]:

170 raise Exception('Override this')

171

172

173 class PhysicalCard(Card):

174 classes: ClassVar[Dict[str, Type[PhysicalCard]]] = {}

175

176 def __eq__(self, other):

177 if not isinstance(other, Card): return False

178 return self.sync_id == other.sync_id

179

180 def __ne__(self, other):

181 return not self.__eq__(other)

182

183 def __hash__(self):

184 return 84065234 + self.sync_id

185

186

187 class VirtualCard(Card, GameViralContext):

188 associated_cards: Sequence[Card]

189 action_params: dict

190 no_reveal: ClassVar[bool] = False

191 game: THBattle

192

193 _suit: Optional[int]

194 _number: Optional[int]

195 _color: Optional[int]

196

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

208

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 }

216

217 def check(self): # override this

218 return False

219

220 @classmethod

221 def unwrap(cls, vcards: Iterable[Card], include_vcards: bool = False) -> List[Card]:

222 lst: List[Card] = []

223 sl = list(vcards)

224

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)

234

235 return lst

236

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

243

244 def get_color(self):

245 if self._color is not None:

246 return self._color

247

248 color = {c.color for c in self.associated_cards}

249 color = color.pop() if len(color) == 1 else Card.NOTSET

250 return color

251

252 def set_color(self, v):

253 self._color = v

254

255 color = property(get_color, set_color)

256

257 def get_number(self):

258 if self._number is not None:

259 return self._number

260

261 num = {c.number for c in self.associated_cards}

262 num = num.pop() if len(num) == 1 else Card.NOTSET

263 return num

264

265 def set_number(self, v):

266 self._number = v

267

268 number = property(get_number, set_number)

269

270 def get_suit(self) -> int:

271 if self._suit is not None:

272 return self._suit

273

274 cl = self.associated_cards

275 suit = cl[0].suit if len(cl) == 1 else Card.NOTSET

276 return suit

277

278 def set_suit(self, v: int):

279 self._suit = v

280

281 suit = property(get_suit, set_suit)

282

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']

288

289 @staticmethod

290 def find_in_hierarchy(card, cls):

291 if card.is_card(cls):

292 return card

293

294 if not card.is_card(VirtualCard):

295 return None

296

297 for c in card.associated_cards:

298 r = VirtualCard.find_in_hierarchy(c, cls)

299 if r: return r

300

301 return None

302

303

304 class CardList(GameObject, deque):

305 DECKCARD = 'deckcard'

306 DROPPEDCARD = 'droppedcard'

307 CARDS = 'cards'

308 SHOWNCARDS = 'showncards'

309 EQUIPS = 'equips'

310 FATETELL = 'fatetell'

311 SPECIAL = 'special'

312

313 def __init__(self, owner: Optional['Character'], typ: str):

314 self.owner = owner

315 self.type = typ

316 deque.__init__(self)

317

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

322

323 def __repr__(self):

324 return "CardList(owner=%s, type=%s, len == %d)" % (self.owner, self.type, len(self))

325

326

327 class 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

332

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)

343

344 def getcards(self, num: int) -> List[Card]:

345 cl = self.cards

346 if len(self.cards) <= num:

347 dcl = self.droppedcards

348

349 assert all(not c.is_card(VirtualCard) for c in dcl)

350 assert all(not c.is_card(ShreddedCard) for c in dcl)

351

352 dropped = list(dcl)

353 dcl.clear()

354 rest = list(cl)

355 cl.clear()

356

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))

361

362 # assert all(not isinstance(c, ShreddedCard) for c in cl)

363 # assert rest == list(cl)[:len(rest)]

364

365 cl = self.cards

366 rst = []

367 for i in range(min(len(cl), num)):

368 rst.append(cl[i])

369

370 return rst

371

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)

375

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

383

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

390

391 def shuffle(self, cl: CardList):

392 owner = cl.owner.player if cl.owner else Nobody()

393

394 g = self.game

395

396 # assert all(c.resides_in is cl for c in cl)

397

398 seed = get_seed_for(g, owner)

399

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]

406

407 for c in cl:

408 c.shred()

409

410 for c in cards:

411 c.resides_in = cl

412

413 cl.clear()

414 cl.extend(cards)

415

416 for c in cl:

417 self.register_card(c)

418

419 # assert all(not isinstance(c, ShreddedCard) for c in cl)

420

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

427

428

429 class Skill(VirtualCard):

430 category: Sequence[str] = ['skill']

431 skill_category: Sequence[str] = []

432

433 ui_meta: ClassVar[SkillMeta]

434

435 def __init__(self, character):

436 assert character is not None

437 VirtualCard.__init__(self, character)

438 self.usage = 'launch'

439

440 def check(self): # override this

441 return False

442

443

444 class TreatAs(object):

445 treat_as: Type[PhysicalCard]

446 usage = 'launch'

447

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)

454

455 def check(self):

456 return False

457

458 def is_card(self, cls):

459 if cls is PhysicalCard:

460 return False

461

462 if issubclass(self.treat_as, cls):

463 return True

464

465 return isinstance(self, cls)

466

467 def target(self: Any, src: Character, tl: Sequence[Character]) -> Tuple[List[Character], bool]:

468 return self.treat_as().target(src, tl)

469

470 def __getattr__(self, name):

471 return getattr(self.treat_as, name)

472

473

474# card targets:

475 def t_None():

476 def t_None(self, src: Character, tl: Sequence[Character]) -> Tuple[List[Character], bool]:

477 return ([], False)

478 return t_None

479

480

481 def t_Self():

482 def t_Self(self, src: Character, tl: Sequence[Character]) -> Tuple[List[Character], bool]:

483 return ([src], True)

484 return t_Self

485

486

487 def 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

496

497

498 def 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

503

504

505 def 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

510

511

512 def 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

518

519

520 def 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)))

528

529 t_OtherLessEqThanN._for_test_OtherLessEqThanN = n # type: ignore

530 return t_OtherLessEqThanN

531

532

533 def 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

538

539

540 def 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))

548

549 t_OtherN._for_test_OtherN = n # type: ignore

550 return t_OtherN

551

552

553 class HiddenCard(Card): # special thing....

554 associated_action = None

555 target = t_None()

556

557

558 class ShreddedCard(Card): # Become this after shuffling

559 associated_action = None

560 target = t_None()

561

562

563 class DummyCard(PhysicalCard): # another special thing....

564 associated_action = None

565 target = t_None()

566 category = ['dummy']

567

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)