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

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: 

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 

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

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

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' 

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 

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 

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 

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 # type: ignore 

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 # type: ignore 

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