# Coverage for game/base.py : 82%

## 582 statements 494 run 88 missing 19 excluded 18 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

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.endpoint import Client # noqa: F401

29

30

31# -- code --

32 log = logging.getLogger('Game')

33

34 all_gameobjects = set()

35 game_objects_hierarchy = set()

36

37

38 class GameObjectMeta(type):

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

40 from utils.codeobj import adjust

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

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

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

44 elif isinstance(v, types.FunctionType):

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

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

47

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

49 all_gameobjects.add(cls)

50 for b in bases:

51 game_objects_hierarchy.add((b, cls))

52

53 return cls

54

55 '''

56 def __getattribute__(cls, name):

57 value = type.__getattribute__(cls, name)

58 if isinstance(value, classmethod):

59 try:

60 rep_class = cls.rep_class(cls)

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

62 except Exception:

63 pass

64

65 return value

66 '''

67

68 @staticmethod

69 def _dump_gameobject_hierarchy():

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

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

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

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

74 for a, b in game_objects_hierarchy

75 ]))

76 f.write('}')

77

78

79 class GameObject(object, metaclass=GameObjectMeta):

80 pass

81

82

83 class TimeLimitExceeded(Timeout, GameObject):

84 pass

85

86

87 class GameException(Exception, GameObject):

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

89 Exception.__init__(self, msg)

90 self.__dict__.update(kwargs)

91

92

93 class GameError(GameException):

94 pass

95

96

97 class GameAbort(GameException):

98 pass

99

100

101 class InterruptActionFlow(GameException):

102 def __init__(self, unwind_to=None):

103 GameException.__init__(self)

104 self.unwind_to = unwind_to

105

106

107 class AssociatedDataViralContext(ViralContext):

108 VIRAL_SEARCH: List[str] = []

109 _: dict

110

111 def viral_import(self, _):

112 self._ = defaultdict(bool)

113

114

115 class Player(GameObject, AssociatedDataViralContext):

116 uid: int

117 name: str

118

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

120 raise GameError('Abstract')

121

122 def __repr__(self):

123 return self.__class__.__name__

124

125

126 class NPC(object):

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

128

129 def __init__(self, name, input_handler):

130 self.name = name

131 self.input_handler = input_handler

132

133

134 class GameEnded(GameException):

135 winners: Sequence[Player]

136

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

138 GameException.__init__(self)

139 self.winners = winners

140

141

142 class GameViralContext(object):

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

144 self = object.__new__(cls)

145 gr = gevent.getcurrent()

146 self.game = gr.game

147 self._ = defaultdict(bool)

148 return self

149

150

151 class Game(GameObject):

152 # ----- Class Variables -----

153 n_persons: ClassVar[int]

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

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

156 bootstrap: ClassVar[Type[BootstrapAction]]

157 dispatcher_cls: ClassVar[Type[EventDispatcher]]

158

159 # ----- Instance Variables -----

160 game: Game

161 runner: GameRunner

162 dispatcher: EventDispatcher

163 event_observer: Optional[EventHandler]

164 action_stack: List[Action]

165 hybrid_stack: List[Union[Action, EventHandler]]

166 ended: bool

167 winners: Sequence[Player]

168 random: Random

169 _: dict

170

171 def __init__(self) -> None:

172 self.game = self

173

174 self.action_stack = []

175 self.hybrid_stack = []

176 self.ended = False

177 self.winners = []

178 self.turn_count = 0

179 self.event_observer = None

180 self.synctag = 0

181

182 self._ = {}

183

184 self.refresh_dispatcher()

185

186 def __repr__(self):

187 return self.__class__.__name__

188

189 def refresh_dispatcher(self) -> None:

190 self.dispatcher = self.dispatcher_cls(self)

191

192 def user_input(

193 self,

194 entities: Sequence[Any],

195 inputlet: Inputlet,

196 timeout: int = 25,

197 type: str = 'single',

198 trans: Optional[InputTransaction] = None,

199 ):

200 return self.runner.user_input(

201 entities, inputlet, timeout, type, trans

202 )

203

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

205 ob = self.event_observer

206 if ob:

207 data = ob.handle(evt_type, data)

208

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

210

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

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

213 return False

214

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

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

217 return action.succeeded

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

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

220 return False

221

222 if not action.can_fire():

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

224 return False

225

226 try:

227 action.succeeded = False

228 except AttributeError:

229 pass

230

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

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

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

234 rst = action.succeeded

235 elif action.cancelled:

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

237 rst = False

238 elif not action.can_fire():

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

240 action.invalid = True

241 rst = False

242 else:

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

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

245 assert not action.cancelled

246 try:

247 self.action_stack.append(action)

248 self.hybrid_stack.append(action)

249 rst = action.apply_action()

250 except InterruptActionFlow as e:

251 if e.unwind_to is action:

252 rst = False

253 else:

254 raise

255 finally:

256 _a = self.action_stack.pop()

257 _b = self.hybrid_stack.pop()

258 assert _a is _b is action

259

260 # If exception occurs here,

261 # the action should be abandoned,

262 # code below makes no sense,

263 # so it's ok to ignore them.

264

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

266 try:

267 action.succeeded = rst

268 except AttributeError:

269 pass

270

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

272

273 rst = action.succeeded

274 action.done = True

275

276 self.emit_event('action_done', action)

277

278 return rst

279

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

281 self.runner.pause(t)

282

283 def get_synctag(self) -> int:

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

285 raise GameAbort

286

287 self.synctag += 1

288 return self.synctag

289

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

291 v = self.runner.is_dropped(p)

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

293 return v

294

295 def is_server_side(self) -> bool:

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

297

298 def is_client_side(self) -> bool:

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

300

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

302 raise GameError('Abstract')

303

304

305 class GameRunner(Greenlet):

306 '''

307 The GameRunner class,

308 Provide interfaces to game environment

309 '''

310

311 def _run(self) -> None:

312 raise GameError('Abstract')

313

314 def user_input(

315 self,

316 entities: Sequence[Any],

317 inputlet: Inputlet,

318 timeout: int = 25,

319 type: str = 'single',

320 trans: Optional[InputTransaction] = None,

321 ):

322 raise GameError('Abstract')

323

324 def is_aborted(self) -> bool:

325 raise GameError('Abstract')

326

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

328 raise GameError('Abstract')

329

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

331 raise GameError('Abstract')

332

333 def get_side(self) -> str:

334 raise GameError('Abstract')

335

336

337 class ActionShootdown(BaseException, GameObject):

338 def __bool__(self):

339 return False

340

341

342 class EventHandler(GameObject):

343 interested: List[str]

344 execute_before: List[str] = []

345 execute_after: List[str] = []

346

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

348

349 def __init__(self, g: Game):

350 self.game = g

351 self._ = {}

352

353 def __repr__(self) -> str:

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

355

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

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

358

359 def get_interested(self):

360 interested = self.interested

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

362 return list(interested)

363

364 @staticmethod

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

366 table = {}

367 eh_classes = set(eh_classes)

368 arbiters: Any = defaultdict(list)

369

370 for cls in eh_classes:

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

372 grp = cls.arbiter if fold_arbiter else None

373 if grp is not None:

374 arbiters[grp].append(cls)

375 cls = grp

376

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

378

379 for a, lst in arbiters.items():

380 eh = table[a.__name__]

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

382

383 allnames = frozenset(table)

384

385 for eh in table.values():

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

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

388

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

390 for before in eh.execute_before:

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

392

393 for after in eh.execute_after:

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

395

396 lst = list(table.values())

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

398

399 toposorted = []

400 while lst:

401 deferred = []

402 commit = []

403 for eh in lst:

404 if not eh.execute_after:

405 for b in eh.execute_before:

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

407 commit.append(eh)

408 else:

409 deferred.append(eh)

410

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

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

413

414 toposorted.extend(commit)

415 lst = deferred

416

417 return toposorted

418

419 @staticmethod

420 def _dump_eh_dependency_graph():

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

422 ehs.remove(EventHandler)

423 dependencies = set()

424 for eh in ehs:

425 for b in eh.execute_before:

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

427

428 for a in eh.execute_after:

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

430

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

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

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

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

435 for a, b in dependencies

436 ]))

437 f.write('}')

438

439

440 class EventArbiter(EventHandler):

441 handlers = ()

442

443 def set_handlers(self, handlers):

444 self.handlers = handlers[:]

445

446

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

448

449

450 class EventDispatcher(GameObject):

451 game: Game

452 _event_handlers: Sequence[EventHandler]

453 _adhoc_ehs: List[EventHandler]

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

455

456 def __init__(self, g: Game):

457 self.game = g

458

459 self._adhoc_ehs = []

460 self._ehs_cache = {}

461 self._event_handlers = self.populate_handlers()

462

463 def add_adhoc(self, eh: EventHandler):

464 self._adhoc_ehs.insert(0, eh)

465

466 def remove_adhoc(self, eh: EventHandler):

467 try:

468 self._adhoc_ehs.remove(eh)

469 except ValueError:

470 pass

471

472 def populate_handlers(self) -> Sequence[EventHandler]:

473 raise Exception('Override this!')

474

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

476 '''

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

478 data can be modified.

479 '''

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

481

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

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

484 s = data

485 else:

486 s = data.__class__.__name__

487

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

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

490 try:

491 co_argnames.remove('self')

492 except Exception:

493 pass

494 co_args = []

495 BAD = object()

496 for n in co_argnames:

497 co_arg = getattr(data, n, BAD)

498 if co_arg is not BAD:

499 co_args.append((n, co_arg))

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

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

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

503 else:

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

505

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

507 action_event = True

508 assert isinstance(data, Action)

509 else:

510 action_event = False

511

512 adhoc = self._adhoc_ehs

513 ehs = self._get_relevant_eh(evt_type)

514

515 for l in adhoc, ehs:

516 for eh in l:

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

518 if action_event and data.cancelled:

519 break

520

521 return data

522

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

524 try:

525 self.game.hybrid_stack.append(eh)

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

527 finally:

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

529 assert rst

530

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

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

533

534 return data

535

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

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

538 if isinstance(c, cls):

539 return c

540

541 return None

542

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

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

545 if isinstance(c, cls):

546 break

547 else:

548 return False

549

550 self._event_handlers.remove(c)

551 for i in c.interested:

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

553

554 return None

555

556 def _get_relevant_eh(self, tag: str):

557 ehs = self._ehs_cache.get(tag)

558 if ehs is not None:

559 return ehs

560

561 ehs = [

562 eh for eh in self._event_handlers if

563 tag in eh.get_interested()

564 ]

565 self._ehs_cache[tag] = ehs

566

567 return ehs

568

569

570 class Action(GameObject, GameViralContext):

571 cancelled = False

572 done = False

573 invalid = False

574 succeeded: bool

575

576 def action_shootdown_exception(self) -> None:

577 if not self.is_valid():

578 raise ActionShootdown(self)

579

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

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

582

583 def action_shootdown(self):

584 try:

585 self.action_shootdown_exception()

586 return None

587

588 except ActionShootdown as e:

589 return e

590

591 def can_fire(self):

592 '''

593 Return true if the action can be fired.

594 '''

595 rst = self.action_shootdown()

596 return True if rst is None else rst

597

598 def apply_action(self):

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

600

601 def is_valid(self):

602 '''

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

604 '''

605 return True

606

607 def __repr__(self):

608 return self.__class__.__name__

609

610

611 class SyncPrimitive(GameObject):

612 def __init__(self, value):

613 self.value = value

614

615 def sync(self, data):

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

617

618 def dump(self):

619 return self.value

620

621 def __repr__(self):

622 return self.value.__repr__()

623

624

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

626

627

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

629 if not to: # sync to nobody

630 return val

631

632 rst: Any = None

633 if isinstance(val, list):

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

635 to.reveal(lst)

636 rst = val.__class__(

637 i.value for i in lst

638 )

639 else:

640 v = SyncPrimitive(val)

641 to.reveal(v)

642 rst = v.value

643

644 return rst

645

646

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

648 if g.is_server_side():

649 seed = g.random.getrandbits(63)

650 else:

651 seed = 0

652

653 return sync_primitive(seed, p)

654

655

656 def list_shuffle(g, lst, plain_to):

657 seed = get_seed_for(g, plain_to)

658

659 if seed: # cardlist owner & server

660 shuffler = random.Random(seed)

661 shuffler.shuffle(lst)

662 else: # others

663 for i in lst:

664 i.conceal()

665

666

667 class Inputlet(GameObject, GameViralContext):

668 '''

669 NOTICE: Inputlet instance variable should

670 not be used as a side channel to pass infomation

671 in game logic code.

672 '''

673 initiator: Any

674 timeout: int

675 actor: object

676

677 @classmethod

678 def tag(cls):

679 clsname = cls.__name__

680 assert clsname.endswith('Inputlet')

681 return clsname[:-8]

682

683 def parse(self, data):

684 '''

685 Process parsed data, return result,

686 return value of this func will be the return value

687 of user_input func.

688 '''

689 return None

690

691 def post_process(self, actor, args):

692 '''

693 This method is called after self.parse succeeded,

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

695 input result before input process finishes.

696 '''

697 return args

698

699 def with_post_process(self, f):

700 '''

701 Helper method, to make this possible

702 @ilet.with_post_process

703 def process(args):

704 ...

705 '''

706 self.post_process = f

707 return f

708

709 def data(self):

710 '''

711 Encode self, used for reconstrcting

712 inputlet state from the other end.

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

714 '''

715 return None

716

717 def __repr__(self):

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

719

720

721 class InputTransaction(GameViralContext):

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

723 self.name = name

724 self.involved = involved[:]

725 self.__dict__.update(kwargs)

726

727 def __enter__(self):

728 return self.begin()

729

730 def begin(self):

731 g = self.game

732 g.emit_event('user_input_transaction_begin', self)

733 return self

734

735 def __exit__(self, *excinfo):

736 self.end()

737 return False

738

739 def end(self):

740 g = self.game

741 g.emit_event('user_input_transaction_end', self)

742

743 def notify(self, evt_name, arg):

744 '''

745 Event For UI

746 '''

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

748

749 def __repr__(self):

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

751

752

753 class Packet(object):

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

755

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

757 self.tag = tag

758 self.data = data

759 self.consumed = False

760

761 def __repr__(self):

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

763

764

765 class GameArchive(TypedDict):

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

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

768

769

770 class GameData(object):

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

772 self.gid = gid

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

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

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

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

777 self._has_data = Event()

778 self._dead = False

779 self._live = live

780 self._tainted = set()

781

782 self._in_gexpect = False

783

784 def feed_recv(self, tag: str, data: object) -> Packet:

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

786 return

787

788 self._seen.add(tag)

789 p = Packet(tag, data)

790 self._recv.append(p)

791 self._pending_recv.append(p)

792 self._has_data.set()

793 return p

794

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

796 p = Packet(tag, data)

797 self._send.append(p)

798 return p

799

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

801 return self._send

802

803 def is_live(self) -> bool:

804 return self._live

805

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

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

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

809

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

811 raise EndpointDied

812

813 try:

814 assert not self._in_gexpect, 'NOT REENTRANT'

815 self._in_gexpect = True

816

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

818 tags = set(tags)

819

820 recv = self._pending_recv

821 e = self._has_data

822 e.clear()

823

824 while True:

825 livepkt = None

826 dropped = False

827 for i, packet in enumerate(recv):

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

829 del recv[i]

830 raise packet

831

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

833 livepkt = packet

834 continue

835

836 if packet.tag in tags:

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

838 del recv[i]

839 packet.consumed = True

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

841 self._live = True

842 tags.discard(packet.tag)

843 self._tainted |= tags

844 return [packet.tag, packet.data]

845 elif packet.tag in self._tainted: 845 ↛ 846 line 845 didn't jump to line 846, because the condition on line 845 was never true

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

847 del recv[i]

848 self._tainted.discard(packet.tag)

849 dropped = True

850 break

851 else:

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

853

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

855 continue

856

857 e.wait(timeout=5)

858 if self._dead:

859 raise EndpointDied

860 e.clear()

861 finally:

862 self._in_gexpect = False

863

864 def die(self) -> None:

865 # Explanation:

866 # When sb. exit game in input state,

867 # the others must wait until his timeout exceeded.

868 # called this to break such condition.

869 self._dead = True

870 self._has_data.set()

871

872 def revive(self) -> None:

873 self._dead = False

874

875 def archive(self) -> GameArchive:

876 return {

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

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

879 }

880

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

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

883 self._recv = recv

884 self._pending_recv = list(recv)

885 self._has_data.set()

886

887

888 class GameItem(object):

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

890

891 # --- class ---

892 key: str = ''

893 args: List[type] = []

894 usable = False

895

896 title = 'ITEM-TITLE'

897 description = 'ITEM-DESC'

898

899 # --- instance ---

900 sku: str

901

902 # --- poison ---

903 init: None

904

905 def __init__(self, *args):

906 raise Exception('Abstract')

907

908 def should_usable(self, g: ServerGame, u: Client) -> None:

909 ...

910

911 @classmethod

912 def register(cls, item_cls):

913 assert issubclass(item_cls, cls)

914 cls.inventory[item_cls.key] = item_cls

915 return item_cls

916

917 @classmethod

918 def from_sku(cls, sku) -> GameItem:

919 if ':' in sku:

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

921 args = args.split(',')

922 else:

923 key = sku

924 args = []

925

926 if key not in cls.inventory:

927 raise exceptions.InvalidItemSKU

928

929 cls = cls.inventory[key]

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

931 raise exceptions.InvalidItemSKU

932

933 try:

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

935 except Exception:

936 raise exceptions.InvalidItemSKU

937

938 o = cls(*args)

939 o.sku = sku

940 return o

941

942

943 class BootstrapAction(Action):

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

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

946 players: BatchList[Player]):

947 raise Exception('Override this!')