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

## 355 statements 326 run 29 missing 10 excluded 8 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

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: 81 ↛ 82 line 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',

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: 100 ↛ 101 line 100 didn't jump to line 101, because the condition on line 100 was never true

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: 123 ↛ 126 line 123 didn't jump to line 126, because the condition on line 123 was never false

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 156 ↛ exit line 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

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 exinwan_target: Optional[Character] # HACK, for ExinwanCard

177

178 def __eq__(self, other):

179 if not isinstance(other, Card): return False 179 ↛ exit line 179 didn't return from function '__eq__', because the return on line 179 wasn't executed

180 return self.sync_id == other.sync_id

181

182 def __ne__(self, other):

183 return not self.__eq__(other)

184

185 def __hash__(self):

186 return 84065234 + self.sync_id

187

188

189 class VirtualCard(Card, GameViralContext):

190 associated_cards: Sequence[Card]

191 action_params: dict

192 no_reveal: ClassVar[bool] = False

193

194 _suit: Optional[int]

195 _number: Optional[int]

196 _color: Optional[int]

197

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

209

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 }

217

218 def check(self): # override this

219 return False

220

221 @classmethod

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

223 lst: List[Card] = []

224 sl = list(vcards)

225

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)

235

236 return lst

237

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

244

245 def get_color(self):

246 if self._color is not None: 246 ↛ 247 line 246 didn't jump to line 247, because the condition on line 246 was never true

247 return self._color

248

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

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

251 return color

252

253 def set_color(self, v):

254 self._color = v

255

256 color = property(get_color, set_color)

257

258 def get_number(self):

259 if self._number is not None: 259 ↛ 260 line 259 didn't jump to line 260, because the condition on line 259 was never true

260 return self._number

261

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

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

264 return num

265

266 def set_number(self, v):

267 self._number = v

268

269 number = property(get_number, set_number)

270

271 def get_suit(self) -> int:

272 if self._suit is not None: 272 ↛ 273 line 272 didn't jump to line 273, because the condition on line 272 was never true

273 return self._suit

274

275 cl = self.associated_cards

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

277 return suit

278

279 def set_suit(self, v: int):

280 self._suit = v

281

282 suit = property(get_suit, set_suit)

283

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

289

290 @staticmethod

291 def find_in_hierarchy(card, cls):

292 if card.is_card(cls):

293 return card

294

295 if not card.is_card(VirtualCard):

296 return None

297

298 for c in card.associated_cards:

299 r = VirtualCard.find_in_hierarchy(c, cls)

300 if r: return r

301

302 return None

303

304

305 class CardList(GameObject, deque):

306 DECKCARD = 'deckcard'

307 DROPPEDCARD = 'droppedcard'

308 CARDS = 'cards'

309 SHOWNCARDS = 'showncards'

310 EQUIPS = 'equips'

311 FATETELL = 'fatetell'

312 SPECIAL = 'special'

313

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

315 self.owner = owner

316 self.type = typ

317 deque.__init__(self)

318

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

323

324 def __repr__(self):

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

326

327

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

333

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)

344

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

346 cl = self.cards

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

348 dcl = self.droppedcards

349

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

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

352

353 dropped = list(dcl)

354 dcl.clear()

355 rest = list(cl)

356 cl.clear()

357

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

362

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

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

365

366 cl = self.cards

367 rst = []

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

369 rst.append(cl[i])

370

371 return rst

372

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)

376

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

384

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

391

392 def shuffle(self, cl: CardList):

393 owner = cl.owner

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

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

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

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(Card): # 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)