# Coverage for game/base.py : 73%

## 590 statements 450 run 140 missing 20 excluded 27 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 defaultdict

6 from random import Random

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

8 from typing import TypeVar, Union

9 import inspect

10 import logging

11 import random

12 import types

13

14# -- third party --

15 from gevent import Greenlet, Timeout

16 from gevent.event import Event

17 from mypy_extensions import TypedDict

18 import gevent

19

20# -- own --

21 from endpoint import EndpointDied

22 from utils.misc import BatchList, exceptions

23 from utils.viral import ViralContext

24

25# -- typing --

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

33 log = logging.getLogger('Game')

34

35 all_gameobjects = set()

36 game_objects_hierarchy = set()

37

38

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

80 class GameObject(object, metaclass=GameObjectMeta):

81 _: dict

82

83

84 class TimeLimitExceeded(Timeout, GameObject):

85 pass

86

87

88 class GameException(Exception, GameObject):

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

90 Exception.__init__(self, msg)

91 self.__dict__.update(kwargs)

92

93

94 class GameError(GameException):

95 pass

96

97

98 class GameAbort(GameException):

99 pass

100

101

102 class InterruptActionFlow(GameException):

103 def __init__(self, unwind_to=None):

104 GameException.__init__(self)

105 self.unwind_to = unwind_to

106

107

108 class AssociatedDataViralContext(ViralContext):

109 VIRAL_SEARCH: List[str] = []

110 _: dict

111

112 def viral_import(self, _):

113 self._ = defaultdict(bool)

114

115

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

127 class Nobody(Player):

128

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

130 pass

131

132

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

141 class GameEnded(GameException):

142 winners: Sequence[Player]

143

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

145 GameException.__init__(self)

146 self.winners = winners

147

148

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

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

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

162

163

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

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

351 class ActionShootdown(BaseException, GameObject):

352 def __bool__(self):

353 return False

354

355

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

454 class EventArbiter(EventHandler):

455 handlers: Sequence[EventHandler] = []

456

457 def set_handlers(self, handlers):

458 self.handlers = handlers[:]

459

460

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

462

463

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

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

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

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

643

644

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

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

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

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

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

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

782 class GameArchive(TypedDict):

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

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

785

786

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

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

961 class 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!')