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 defaultdict 

6from random import Random 

7from typing import Any, ClassVar, Dict, List, Optional, Sequence, Set, TYPE_CHECKING, Tuple, Type, Generic 

8from typing import TypeVar, Union 

9import inspect 

10import logging 

11import random 

12import types 

13 

14# -- third party -- 

15from gevent import Greenlet, Timeout 

16from gevent.event import Event 

17from mypy_extensions import TypedDict 

18import gevent 

19 

20# -- own -- 

21from endpoint import EndpointDied 

22from utils.misc import BatchList, exceptions 

23from utils.viral import ViralContext 

24 

25# -- typing -- 

26if TYPE_CHECKING: 

27 from server.base import Game as ServerGame # noqa: F401 

28 from server.core import Core as ServerCore # noqa: F401 

29 from server.endpoint import Client # noqa: F401 

30 

31 

32# -- code -- 

33log = logging.getLogger('Game') 

34 

35all_gameobjects = set() 

36game_objects_hierarchy = set() 

37 

38 

39class GameObjectMeta(type): 

40 def __new__(mcls, clsname, bases, kw): 

41 from utils.codeobj import adjust 

42 for k, v in kw.items(): 

43 if isinstance(v, (list, set)): 

44 kw[k] = tuple(v) # mutable obj not allowed 

45 elif isinstance(v, types.FunctionType): 

46 v.__name__ = f'{clsname}.{v.__name__}' 

47 v.__code__ = adjust(v.__code__, name=v.__name__) 

48 

49 cls = super().__new__(mcls, clsname, bases, kw) 

50 all_gameobjects.add(cls) 

51 for b in bases: 

52 game_objects_hierarchy.add((b, cls)) 

53 

54 return cls 

55 

56 ''' 

57 def __getattribute__(cls, name): 

58 value = type.__getattribute__(cls, name) 

59 if isinstance(value, classmethod): 

60 try: 

61 rep_class = cls.rep_class(cls) 

62 return lambda *a, **k: value.__get__(None, rep_class) 

63 except Exception: 

64 pass 

65 

66 return value 

67 ''' 

68 

69 @staticmethod 

70 def _dump_gameobject_hierarchy(): 

71 with open('/dev/shm/gomap.dot', 'w') as f: 

72 f.write('digraph {\nrankdir=LR;\n') 

73 f.write('\n'.join([ 

74 '"%s" -> "%s";' % (a.__name__, b.__name__) 

75 for a, b in game_objects_hierarchy 

76 ])) 

77 f.write('}') 

78 

79 

80class GameObject(object, metaclass=GameObjectMeta): 

81 _: dict 

82 

83 

84class TimeLimitExceeded(Timeout, GameObject): 

85 pass 

86 

87 

88class GameException(Exception, GameObject): 

89 def __init__(self, msg=None, **kwargs): 

90 Exception.__init__(self, msg) 

91 self.__dict__.update(kwargs) 

92 

93 

94class GameError(GameException): 

95 pass 

96 

97 

98class GameAbort(GameException): 

99 pass 

100 

101 

102class InterruptActionFlow(GameException): 

103 def __init__(self, unwind_to=None): 

104 GameException.__init__(self) 

105 self.unwind_to = unwind_to 

106 

107 

108class AssociatedDataViralContext(ViralContext): 

109 VIRAL_SEARCH: List[str] = [] 

110 _: dict 

111 

112 def viral_import(self, _): 

113 self._ = defaultdict(bool) 

114 

115 

116class Player(GameObject, AssociatedDataViralContext): 

117 uid: int 

118 name: str 

119 

120 def reveal(self, obj_list: Any) -> None: 

121 raise GameError('Abstract') 

122 

123 def __repr__(self): 

124 return self.__class__.__name__ 

125 

126 

127class Nobody(Player): 

128 

129 def reveal(self, obj_list: Any) -> None: 

130 pass 

131 

132 

133class NPC(object): 

134 __slots__ = ('name', 'input_handler') 

135 

136 def __init__(self, name, input_handler): 

137 self.name = name 

138 self.input_handler = input_handler 

139 

140 

141class GameEnded(GameException): 

142 winners: Sequence[Player] 

143 

144 def __init__(self, winners: Sequence[Player]): 

145 GameException.__init__(self) 

146 self.winners = winners 

147 

148 

149class GameViralContext(object): 

150 game: Game 

151 

152 def __new__(cls, *a, **k): 

153 self = object.__new__(cls) 

154 gr = gevent.getcurrent() 

155 self.game = gr.game 

156 self._ = defaultdict(bool) 

157 return self 

158 

159 

160A = TypeVar('A', bound='Action') 

161EH = TypeVar('EH', bound='EventHandler') 

162 

163 

164class Game(GameObject, Generic[A, EH]): 

165 # ----- Class Variables ----- 

166 n_persons: ClassVar[int] 

167 npc_players: ClassVar[List[NPC]] = [] 

168 params_def: ClassVar[Dict[str, Any]] = {} 

169 bootstrap: ClassVar[Type[BootstrapAction]] 

170 dispatcher_cls: ClassVar[Type[EventDispatcher]] 

171 

172 # ----- Instance Variables ----- 

173 game: Game 

174 runner: GameRunner 

175 dispatcher: EventDispatcher 

176 event_observer: Optional[EventHandler] 

177 action_stack: List[A] 

178 hybrid_stack: List[Union[A, EH]] 

179 ended: bool 

180 winners: Sequence[Player] 

181 random: Random 

182 _: dict 

183 

184 def __init__(self) -> None: 

185 self.game = self 

186 

187 self.action_stack = [] 

188 self.hybrid_stack = [] 

189 self.ended = False 

190 self.winners = [] 

191 self.turn_count = 0 

192 self.event_observer = None 

193 self.synctag = 0 

194 

195 self._ = {} 

196 

197 self.refresh_dispatcher() 

198 

199 def __repr__(self): 

200 return self.__class__.__name__ 

201 

202 def refresh_dispatcher(self) -> None: 

203 self.dispatcher = self.dispatcher_cls(self) 

204 

205 def user_input( 

206 self, 

207 entities: Sequence[Any], 

208 inputlet: Inputlet, 

209 timeout: int = 25, 

210 type: str = 'single', 

211 trans: Optional[InputTransaction] = None, 

212 ): 

213 return self.runner.user_input( 

214 entities, inputlet, timeout, type, trans 

215 ) 

216 

217 def emit_event(self, evt_type: str, data: Any) -> Any: 

218 ob = self.event_observer 

219 if ob: 

220 data = ob.handle(evt_type, data) 

221 

222 return self.dispatcher.emit(evt_type, data) 

223 

224 def process_action(self, action: Action) -> bool: 

225 if self.ended: 225 ↛ 226line 225 didn't jump to line 226, because the condition on line 225 was never true

226 return False 

227 

228 if action.done: 228 ↛ 229line 228 didn't jump to line 229, because the condition on line 228 was never true

229 log.debug('action already done %s' % action.__class__.__name__) 

230 return action.succeeded 

231 elif action.cancelled or action.invalid: 231 ↛ 232line 231 didn't jump to line 232, because the condition on line 231 was never true

232 log.debug('action cancelled/invalid %s' % action.__class__.__name__) 

233 return False 

234 

235 if not action.can_fire(): 235 ↛ 236line 235 didn't jump to line 236, because the condition on line 235 was never true

236 log.debug('action invalid %s' % action.__class__.__name__) 

237 return False 

238 

239 try: 

240 action.succeeded = False 

241 except AttributeError: 

242 pass 

243 

244 action = self.emit_event('action_before', action) 

245 if action.done: 245 ↛ 246line 245 didn't jump to line 246, because the condition on line 245 was never true

246 log.debug('action already done %s' % action.__class__.__name__) 

247 rst = action.succeeded 

248 elif action.cancelled: 248 ↛ 249line 248 didn't jump to line 249, because the condition on line 248 was never true

249 log.debug('action cancelled, not firing: %s' % action.__class__.__name__) 

250 rst = False 

251 elif not action.can_fire(): 251 ↛ 252line 251 didn't jump to line 252, because the condition on line 251 was never true

252 log.debug('action invalid, not firing: %s' % action.__class__.__name__) 

253 action.invalid = True 

254 rst = False 

255 else: 

256 log.debug('applying action %s' % action.__class__.__name__) 

257 action = self.emit_event('action_apply', action) 

258 assert not action.cancelled 

259 try: 

260 self.action_stack.append(action) 

261 self.hybrid_stack.append(action) 

262 hybrid = self.hybrid_stack # noqa, when crashes pytest will show hybrid stack here by inspecting local variables 

263 rst = action.apply_action() 

264 except InterruptActionFlow as e: 

265 if e.unwind_to is action: 

266 rst = False 

267 else: 

268 raise 

269 finally: 

270 _a = self.action_stack.pop() 

271 _b = self.hybrid_stack.pop() 

272 assert _a is _b is action 

273 

274 # If exception occurs here, 

275 # the action should be abandoned, 

276 # code below makes no sense, 

277 # so it's ok to ignore them. 

278 

279 assert rst in (True, False), 'Action.apply_action must return boolean!' 

280 try: 

281 action.succeeded = rst 

282 except AttributeError: 

283 pass 

284 

285 action = self.emit_event('action_after', action) 

286 

287 rst = action.succeeded 

288 action.done = True 

289 

290 self.emit_event('action_done', action) 

291 

292 return rst 

293 

294 def pause(self, t: float) -> None: 

295 self.runner.pause(t) 

296 

297 def get_synctag(self) -> int: 

298 if self.runner.is_aborted(): 298 ↛ 299line 298 didn't jump to line 299, because the condition on line 298 was never true

299 raise GameAbort 

300 

301 self.synctag += 1 

302 return self.synctag 

303 

304 def is_dropped(self, p: Player) -> bool: 

305 v = self.runner.is_dropped(p) 

306 log.debug('Game.is_dropped(%s) == %s', p, v) 

307 return v 

308 

309 def is_server_side(self) -> bool: 

310 return self.runner.get_side() == 'server' 

311 

312 def is_client_side(self) -> bool: 

313 return self.runner.get_side() == 'client' 

314 

315 def can_leave(self, p: Player) -> bool: 

316 raise GameError('Abstract') 

317 

318 

319class GameRunner(Greenlet): 

320 ''' 

321 The GameRunner class, 

322 Provide interfaces to game environment 

323 ''' 

324 

325 def _run(self) -> None: 

326 raise GameError('Abstract') 

327 

328 def user_input( 

329 self, 

330 entities: Sequence[Any], 

331 inputlet: Inputlet, 

332 timeout: int = 25, 

333 type: str = 'single', 

334 trans: Optional[InputTransaction] = None, 

335 ): 

336 raise GameError('Abstract') 

337 

338 def is_aborted(self) -> bool: 

339 raise GameError('Abstract') 

340 

341 def is_dropped(self, p: Player) -> bool: 

342 raise GameError('Abstract') 

343 

344 def pause(self, time: float) -> None: 

345 raise GameError('Abstract') 

346 

347 def get_side(self) -> str: 

348 raise GameError('Abstract') 

349 

350 

351class ActionShootdown(BaseException, GameObject): 

352 def __bool__(self): 

353 return False 

354 

355 

356class EventHandler(GameObject): 

357 interested: List[str] 

358 execute_before: List[str] = [] 

359 execute_after: List[str] = [] 

360 

361 arbiter: Optional[Type[EventArbiter]] = None 

362 

363 def __init__(self, g: Game): 

364 self.game = g 

365 self._ = {} 

366 

367 def __repr__(self) -> str: 

368 return f'EV:{self.__class__.__name__}' 

369 

370 def handle(self, evt_type: str, data: Any): 

371 raise GameError('Override handle function to implement EventHandler logics!') 

372 

373 def get_interested(self): 

374 interested = self.interested 

375 assert isinstance(interested, (list, tuple)), "Should specify interested events! %r" % self.__class__ 

376 return list(interested) 

377 

378 @staticmethod 

379 def make_list(g, eh_classes, fold_arbiter=True): 

380 table = {} 

381 eh_classes = set(eh_classes) 

382 arbiters: Any = defaultdict(list) 

383 

384 for cls in eh_classes: 

385 assert not issubclass(cls, EventArbiter), 'Should not pass arbiters in make_list, %r' % cls 

386 grp = cls.arbiter if fold_arbiter else None 

387 if grp is not None: 387 ↛ 388line 387 didn't jump to line 388, because the condition on line 387 was never true

388 arbiters[grp].append(cls) 

389 cls = grp 

390 

391 table[cls.__name__] = cls(g) 

392 

393 for a, lst in arbiters.items(): 393 ↛ 394line 393 didn't jump to line 394, because the loop on line 393 never started

394 eh = table[a.__name__] 

395 eh.set_handlers(EventHandler.make_list(g, lst, fold_arbiter=False)) 

396 

397 allnames = frozenset(table) 

398 

399 for eh in table.values(): 

400 eh.execute_before = set(eh.execute_before) & allnames # make it instance var 

401 eh.execute_after = set(eh.execute_after) & allnames 

402 

403 for clsname, eh in table.items(): 

404 for before in eh.execute_before: 

405 table[before].execute_after.add(clsname) 

406 

407 for after in eh.execute_after: 

408 table[after].execute_before.add(clsname) 

409 

410 lst = list(table.values()) 

411 lst.sort(key=lambda v: v.__class__.__name__) # must sync between server and client 

412 

413 toposorted = [] 

414 while lst: 

415 deferred = [] 

416 commit = [] 

417 for eh in lst: 

418 if not eh.execute_after: 

419 for b in eh.execute_before: 

420 table[b].execute_after.remove(eh.__class__.__name__) 

421 commit.append(eh) 

422 else: 

423 deferred.append(eh) 

424 

425 if not commit: 425 ↛ 426line 425 didn't jump to line 426, because the condition on line 425 was never true

426 raise GameError("Can't resolve dependencies! Check for circular reference!") 

427 

428 toposorted.extend(commit) 

429 lst = deferred 

430 

431 return toposorted 

432 

433 @staticmethod 

434 def _dump_eh_dependency_graph(): 

435 ehs: Set[Type[EventHandler]] = {i for i in all_gameobjects if issubclass(i, EventHandler)} 

436 ehs.remove(EventHandler) 

437 dependencies = set() 

438 for eh in ehs: 

439 for b in eh.execute_before: 

440 dependencies.add((eh.__name__, b)) 

441 

442 for a in eh.execute_after: 

443 dependencies.add((a, eh.__name__)) 

444 

445 with open('/dev/shm/eh_relations.dot', 'w') as f: 

446 f.write('digraph {\nrankdir=LR;\n') 

447 f.write('\n'.join([ 

448 '%s -> %s;' % (a, b) 

449 for a, b in dependencies 

450 ])) 

451 f.write('}') 

452 

453 

454class EventArbiter(EventHandler): 

455 handlers: Sequence[EventHandler] = [] 

456 

457 def set_handlers(self, handlers): 

458 self.handlers = handlers[:] 

459 

460 

461T = TypeVar('T', bound=EventHandler) 

462 

463 

464class EventDispatcher(GameObject): 

465 game: Game 

466 _event_handlers: List[EventHandler] 

467 _adhoc_ehs: List[EventHandler] 

468 _ehs_cache: Dict[str, List[EventHandler]] 

469 

470 def __init__(self, g: Game): 

471 self.game = g 

472 

473 self._adhoc_ehs = [] 

474 self._ehs_cache = {} 

475 self._event_handlers = self.populate_handlers() 

476 

477 def add_adhoc(self, eh: EventHandler): 

478 self._adhoc_ehs.insert(0, eh) 

479 

480 def remove_adhoc(self, eh: EventHandler): 

481 try: 

482 self._adhoc_ehs.remove(eh) 

483 except ValueError: 

484 pass 

485 

486 def populate_handlers(self) -> List[EventHandler]: 

487 raise Exception('Override this!') 

488 

489 def emit(self, evt_type: str, data: Any): 

490 ''' 

491 Fire an event, all relevant event handlers will see this, 

492 data can be modified. 

493 ''' 

494 random.random() < 0.01 and gevent.idle() # prevent buggy logic code block scheduling 

495 

496 if log.isEnabledFor(logging.DEBUG): 496 ↛ 497line 496 didn't jump to line 497, because the condition on line 496 was never true

497 if isinstance(data, (list, tuple, str)) or hasattr(data, '__dataclass_fields__'): 

498 s = data 

499 else: 

500 s = data.__class__.__name__ 

501 

502 if evt_type in ('action_before', 'action_apply', 'action_after'): 

503 co_argnames = inspect.getfullargspec(data.__init__).args 

504 try: 

505 co_argnames.remove('self') 

506 except Exception: 

507 pass 

508 co_args = [] 

509 BAD = object() 

510 for n in co_argnames: 

511 co_arg = getattr(data, n, BAD) 

512 if co_arg is not BAD: 

513 co_args.append((n, co_arg)) 

514 co_argsstr = ', '.join(f'{name}={repr(a)}' for name, a in co_args) 

515 act_repr = f'{s}({co_argsstr})' 

516 log.debug('emit_event: %s %s', evt_type, act_repr) 

517 else: 

518 log.debug('emit_event: %s %s', evt_type, s) 

519 

520 if evt_type in ('action_before', 'action_apply', 'action_after'): 

521 action_event = True 

522 assert isinstance(data, Action) 

523 else: 

524 action_event = False 

525 

526 adhoc = self._adhoc_ehs 

527 ehs = self._get_relevant_eh(evt_type) 

528 

529 for l in adhoc, ehs: 

530 for eh in l: 

531 data = self.handle_single_event(eh, evt_type, data) 

532 if action_event and data.cancelled: 532 ↛ 533line 532 didn't jump to line 533, because the condition on line 532 was never true

533 break 

534 

535 return data 

536 

537 def handle_single_event(self, eh: EventHandler, *a, **k): 

538 try: 

539 self.game.hybrid_stack.append(eh) 

540 data = eh.handle(*a, **k) 

541 finally: 

542 rst = eh is self.game.hybrid_stack.pop() 

543 assert rst 

544 

545 if data is None: 545 ↛ 546line 545 didn't jump to line 546, because the condition on line 545 was never true

546 raise Exception('EventHandler %s returned None' % eh.__class__.__name__) 

547 

548 return data 

549 

550 def find_by_cls(self, cls: Type[T]) -> Optional[T]: 

551 for c in self._event_handlers: 

552 if isinstance(c, cls): 

553 return c 

554 

555 return None 

556 

557 def remove_by_cls(self, cls: Type[T]) -> bool: 

558 c: Any # stupid mypy 

559 

560 for c in self._event_handlers: 560 ↛ 564line 560 didn't jump to line 564, because the loop on line 560 didn't complete

561 if isinstance(c, cls): 

562 break 

563 else: 

564 return False 

565 

566 for i in c.interested: 

567 self._ehs_cache.pop(i, '') 

568 

569 self._event_handlers.remove(c) 

570 

571 return True 

572 

573 def _get_relevant_eh(self, tag: str): 

574 ehs = self._ehs_cache.get(tag) 

575 if ehs is not None: 

576 return ehs 

577 

578 ehs = [ 

579 eh for eh in self._event_handlers if 

580 tag in eh.get_interested() 

581 ] 

582 self._ehs_cache[tag] = ehs 

583 

584 return ehs 

585 

586 

587class Action(GameObject, GameViralContext): 

588 cancelled = False 

589 done = False 

590 invalid = False 

591 succeeded: bool 

592 

593 def action_shootdown_exception(self) -> None: 

594 if not self.is_valid(): 594 ↛ 595line 594 didn't jump to line 595, because the condition on line 594 was never true

595 raise ActionShootdown(self) 

596 

597 _ = self.game.emit_event('action_shootdown', self) 

598 assert _ is self, "You can't replace action in 'action_shootdown' event!" 

599 

600 def action_shootdown(self): 

601 try: 

602 self.action_shootdown_exception() 

603 return None 

604 

605 except ActionShootdown as e: 

606 return e 

607 

608 def can_fire(self): 

609 ''' 

610 Return true if the action can be fired. 

611 ''' 

612 rst = self.action_shootdown() 

613 return True if rst is None else rst 

614 

615 def apply_action(self): 

616 raise GameError('Override apply_action to implement Action logics!') 

617 

618 def is_valid(self): 

619 ''' 

620 Return True if this action is complete and ready to fire. 

621 ''' 

622 return True 

623 

624 def __repr__(self): 

625 return self.__class__.__name__ 

626 

627 

628class SyncPrimitive(GameObject): 

629 def __init__(self, value): 

630 self.value = value 

631 

632 def sync(self, data): 

633 self.value = self.value.__class__(data) 

634 

635 def dump(self): 

636 return self.value 

637 

638 def __repr__(self): 

639 return self.value.__repr__() 

640 

641 

642T_sync = TypeVar('T_sync', bound=Union[list, int, str, bool]) 

643 

644 

645def sync_primitive(val: T_sync, to: Union[Player, BatchList[Player]]) -> T_sync: 

646 if not to: # sync to nobody 646 ↛ 647line 646 didn't jump to line 647, because the condition on line 646 was never true

647 return val 

648 

649 rst: Any = None 

650 if isinstance(val, list): 

651 lst = [SyncPrimitive(i) for i in val] 

652 to.reveal(lst) 

653 rst = val.__class__( 

654 i.value for i in lst 

655 ) 

656 else: 

657 v = SyncPrimitive(val) 

658 to.reveal(v) 

659 rst = v.value 

660 

661 return rst 

662 

663 

664def get_seed_for(g: Game, p: Union[Player, BatchList[Player]]): 

665 if g.is_server_side(): 665 ↛ 668line 665 didn't jump to line 668, because the condition on line 665 was never false

666 seed = g.random.getrandbits(63) 

667 else: 

668 seed = 0 

669 

670 return sync_primitive(seed, p) 

671 

672 

673def list_shuffle(g, lst, plain_to): 

674 seed = get_seed_for(g, plain_to) 

675 

676 if seed: # cardlist owner & server 

677 shuffler = random.Random(seed) 

678 shuffler.shuffle(lst) 

679 else: # others 

680 for i in lst: 

681 i.conceal() 

682 

683 

684class Inputlet(GameObject, GameViralContext): 

685 ''' 

686 NOTICE: Inputlet instance variable should 

687 not be used as a side channel to pass infomation 

688 in game logic code. 

689 ''' 

690 initiator: Any 

691 timeout: int 

692 actor: Any 

693 

694 @classmethod 

695 def tag(cls): 

696 clsname = cls.__name__ 

697 assert clsname.endswith('Inputlet') 

698 return clsname[:-8] 

699 

700 def parse(self, data): 

701 ''' 

702 Process parsed data, return result, 

703 return value of this func will be the return value 

704 of user_input func. 

705 ''' 

706 return None 

707 

708 def post_process(self, actor, args): 

709 ''' 

710 This method is called after self.parse succeeded, 

711 so game logic may have chance to transform (and validate) 

712 input result before input process finishes. 

713 ''' 

714 return args 

715 

716 def with_post_process(self, f): 

717 ''' 

718 Helper method, to make this possible 

719 @ilet.with_post_process 

720 def process(args): 

721 ... 

722 ''' 

723 self.post_process = f # type: ignore 

724 return f 

725 

726 def data(self): 

727 ''' 

728 Encode self, used for reconstrcting 

729 inputlet state from the other end. 

730 Will be fed into self.process() of the other end. 

731 ''' 

732 return None 

733 

734 def __repr__(self): 

735 return f'<I:{self.tag()}>' 

736 

737 

738class InputTransaction(GameViralContext): 

739 def __init__(self, name: str, involved: Sequence[Any], **kwargs): 

740 self.name = name 

741 self.involved = involved[:] 

742 self.__dict__.update(kwargs) 

743 

744 def __enter__(self): 

745 return self.begin() 

746 

747 def begin(self): 

748 g = self.game 

749 g.emit_event('user_input_transaction_begin', self) 

750 return self 

751 

752 def __exit__(self, *excinfo): 

753 self.end() 

754 return False 

755 

756 def end(self): 

757 g = self.game 

758 g.emit_event('user_input_transaction_end', self) 

759 

760 def notify(self, evt_name, arg): 

761 ''' 

762 Event For UI 

763 ''' 

764 self.game.emit_event('user_input_transaction_feedback', (self, evt_name, arg)) 

765 

766 def __repr__(self): 

767 return '<T:{}>'.format(self.name) 

768 

769 

770class Packet(object): 

771 __slots__ = ('tag', 'data', 'consumed') 

772 

773 def __init__(self, tag: str, data: object): 

774 self.tag = tag 

775 self.data = data 

776 self.consumed = False 

777 

778 def __repr__(self): 

779 return 'Packet<%s, %s, %s>' % (self.tag, self.data, '_X'[self.consumed]) 

780 

781 

782class GameArchive(TypedDict): 

783 send: List[Tuple[str, Any]] # tag, data 

784 recv: List[Tuple[str, Any]] # tag, data 

785 

786 

787class GameData(object): 

788 def __init__(self, gid: int, live=True): 

789 self.gid = gid 

790 self._send: List[Packet] = [] 

791 self._recv: List[Packet] = [] 

792 self._seen: Set[str] = set() 

793 self._pending_recv: List[Packet] = [] 

794 self._has_data = Event() 

795 self._dead = False 

796 self._live = live 

797 self._tainted: Set[str] = set() 

798 

799 self._in_gexpect = False 

800 

801 def feed_recv(self, tag: str, data: object) -> Optional[Packet]: 

802 if tag in self._seen: 802 ↛ 803line 802 didn't jump to line 803, because the condition on line 802 was never true

803 return None 

804 

805 self._seen.add(tag) 

806 p = Packet(tag, data) 

807 self._recv.append(p) 

808 self._pending_recv.append(p) 

809 self._has_data.set() 

810 return p 

811 

812 def feed_send(self, tag: str, data: object): 

813 p = Packet(tag, data) 

814 self._send.append(p) 

815 return p 

816 

817 def get_sent(self) -> List[Packet]: 

818 return self._send 

819 

820 def is_live(self) -> bool: 

821 return self._live 

822 

823 def gexpect(self, tags: Sequence[str]): 

824 assert not isinstance(tags, str) # legacy usage 

825 assert '*' not in tags[0] # legacy usage 

826 

827 if self._dead: 827 ↛ 828line 827 didn't jump to line 828, because the condition on line 827 was never true

828 raise EndpointDied 

829 

830 try: 

831 assert not self._in_gexpect, 'NOT REENTRANT' 

832 self._in_gexpect = True 

833 

834 log.debug('GAME_EXPECT: %s', repr(tags)) 

835 

836 tags = set(tags) # type: ignore 

837 assert isinstance(tags, set) # stupid mypy 

838 

839 recv = self._pending_recv 

840 e = self._has_data 

841 e.clear() 

842 

843 while True: 

844 livepkt = None 

845 dropped = False 

846 for i, packet in enumerate(recv): 

847 if isinstance(packet, EndpointDied): 847 ↛ 848line 847 didn't jump to line 848, because the condition on line 847 was never true

848 del recv[i] 

849 raise packet 

850 

851 if packet.tag == '__game_live': 851 ↛ 852line 851 didn't jump to line 852, because the condition on line 851 was never true

852 livepkt = packet 

853 continue 

854 

855 if packet.tag in tags: 855 ↛ 864line 855 didn't jump to line 864, because the condition on line 855 was never false

856 log.debug('GAME_READ: %s', repr(packet)) 

857 del recv[i] 

858 packet.consumed = True 

859 if livepkt: 859 ↛ 860line 859 didn't jump to line 860, because the condition on line 859 was never true

860 self._live = True 

861 tags.discard(packet.tag) 

862 self._tainted |= tags 

863 return [packet.tag, packet.data] 

864 elif packet.tag in self._tainted: 

865 log.warning('GAME_DROP: %s, GID: %s', repr(packet), self.gid) 

866 del recv[i] 

867 self._tainted.discard(packet.tag) 

868 dropped = True 

869 break 

870 else: 

871 log.debug('GAME_MISS: %s, EXPECTS: %s, GID: %s', repr(packet), tags, self.gid) 

872 

873 if dropped: 873 ↛ 874line 873 didn't jump to line 874, because the condition on line 873 was never true

874 continue 

875 

876 e.wait(timeout=5) 

877 if self._dead: 877 ↛ 879line 877 didn't jump to line 879, because the condition on line 877 was never false

878 raise EndpointDied 

879 e.clear() 

880 finally: 

881 self._in_gexpect = False 

882 

883 def die(self) -> None: 

884 # Explanation: 

885 # When sb. exit game in input state, 

886 # the others must wait until his timeout exceeded. 

887 # called this to break such condition. 

888 self._dead = True 

889 self._has_data.set() 

890 

891 def revive(self) -> None: 

892 self._dead = False 

893 

894 def archive(self) -> GameArchive: 

895 return { 

896 'send': [(i.tag, i.data) for i in self._send], 

897 'recv': [(i.tag, i.data) for i in self._recv], 

898 } 

899 

900 def feed_archive(self, data: GameArchive) -> None: 

901 recv = [Packet(t, d) for t, d in data['recv']] 

902 self._recv = recv 

903 self._pending_recv = list(recv) 

904 self._has_data.set() 

905 

906 

907class GameItem(object): 

908 inventory: Dict[str, Type[GameItem]] = {} 

909 

910 # --- class --- 

911 key: ClassVar[str] = '' 

912 args: ClassVar[List[type]] = [] 

913 usable: ClassVar[bool] = False 

914 

915 # --- instance --- 

916 sku: str 

917 title: str = 'ITEM-TITLE' 

918 description: str = 'ITEM-DESC' 

919 

920 # --- poison --- 

921 init: None 

922 

923 def __init__(self, *args): 

924 raise Exception('Abstract') 

925 

926 def should_usable(self, core: ServerCore, g: ServerGame, u: Client) -> None: 

927 ... 

928 

929 @classmethod 

930 def register(cls, item_cls): 

931 assert issubclass(item_cls, cls) 

932 cls.inventory[item_cls.key] = item_cls 

933 return item_cls 

934 

935 @classmethod 

936 def from_sku(cls, sku: str) -> GameItem: 

937 if ':' in sku: 

938 key, args = sku.split(':') 

939 args = args.split(',') 

940 else: 

941 key = sku 

942 args = [] 

943 

944 if key not in cls.inventory: 

945 raise exceptions.InvalidItemSKU 

946 

947 cls = cls.inventory[key] 

948 if len(cls.args) != len(args): 948 ↛ 949line 948 didn't jump to line 949, because the condition on line 948 was never true

949 raise exceptions.InvalidItemSKU 

950 

951 try: 

952 args = [T(v) for T, v in zip(cls.args, args)] 

953 except Exception: 

954 raise exceptions.InvalidItemSKU 

955 

956 o = cls(*args) 

957 o.sku = sku 

958 return o 

959 

960 

961class BootstrapAction(Action): 

962 def __init__(self, params: Dict[str, Any], 

963 items: Dict[Player, List[GameItem]], 

964 players: BatchList[Player]): 

965 raise Exception('Override this!')