Hide keyboard shortcuts

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 

3 

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 

12 

13# -- third party -- 

14# -- own -- 

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

16from thb.mode import THBattle 

17 

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 

23 

24 

25# -- code -- 

26log = logging.getLogger('THBattle_Cards') 

27alloc_id = itertools.count(1).__next__ 

28 

29 

30class 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 ↛ 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', 

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 ↛ 101line 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 ↛ 126line 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 ↛ 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 

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 

173class 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 ↛ 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 

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 

189class 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 ↛ 247line 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: 

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 ↛ 273line 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 

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' 

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 

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 

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 

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

444class 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: 

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

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 

485 

486 

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 

496 

497 

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 

503 

504 

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 

510 

511 

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 

518 

519 

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

528 

529 t_OtherLessEqThanN._for_test_OtherLessEqThanN = n 

530 return t_OtherLessEqThanN 

531 

532 

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 

538 

539 

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

548 

549 t_OtherN._for_test_OtherN = n 

550 return t_OtherN 

551 

552 

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

554 associated_action = None 

555 target = t_None() 

556 

557 

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

559 associated_action = None 

560 target = t_None() 

561 

562 

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