# Coverage for server/parts/room.py : 87%

## 313 statements 285 run 28 missing 4 excluded 23 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 typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple

7 import logging

8 import time

9

10# -- third party --

11 from gevent import Greenlet

12 from mypy_extensions import TypedDict

13

14# -- own --

15 from endpoint import Endpoint

16 from game.base import Game, GameAbort

17 from server.base import ServerGameRunner

18 from server.endpoint import Client

19 from server.utils import command

20 from utils.misc import BatchList, throttle

21 import wire

22

23

24# -- code --

25 if TYPE_CHECKING:

26 from server.core import Core # noqa: F401

27

28

29# -- code --

30 log = logging.getLogger('Room')

31

32

33 class RoomAssocOnGame(TypedDict):

34 gid: int

35 users: BatchList[Client]

36 left: Dict[Client, bool]

37 name: str

38 flags: wire.msg.CreateRoomFlags

39 start_time: float

40 greenlet: Optional[Greenlet]

41

42 _notifier: Optional[Greenlet]

43

44

45 def Ag(self: Room, g: Game) -> RoomAssocOnGame:

46 return g._[self]

47

48

49 class Room(object):

50

51 def __init__(self, core: Core):

52 self.core = core

53

54 core.events.user_state_transition += self.handle_user_state_transition

55 core.events.game_started += self.handle_game_started

56 core.events.game_joined += self.handle_game_joined

57 core.events.game_left += self.handle_game_left

58 core.events.game_ended += self.handle_game_ended

59 core.events.core_initialized += self.handle_core_initialized

60

61 D = core.events.client_command

62 D[wire.CreateRoom] += self._create

63 D[wire.JoinRoom] += self._join

64 D[wire.LeaveRoom] += self._leave

65 D[wire.GetRoomUsers] += self._users

66 D[wire.GetReady] += self._get_ready

67 D[wire.SetGameParam] += self._set_param

68 D[wire.ChangeLocation] += self._change_location

69 D[wire.CancelReady] += self._cancel_ready

70

71 self.games: Dict[int, Game] = {}

72

73 def __repr__(self) -> str:

74 return self.__class__.__name__

75

76 def handle_core_initialized(self, core: Core) -> Core:

77 self._init_freeslot()

78 return core

79

80 def handle_user_state_transition(self, ev: Tuple[Client, str, str]) -> Tuple[Client, str, str]:

81 c, f, t = ev

82 core = self.core

83

84 if (f, t) == ('uninitialized', 'freeslot'):

85 # Just don't bother, core is not running at this time

86 return ev

87

88 if t == 'dropped' and f in ('room', 'ready', 'game'): 88 ↛ 89line 88 didn't jump to line 89, because the condition on line 88 was never true

89 self.exit_game(c)

90

91 if f in ('room', 'ready', 'game') or \

92 t in ('room', 'ready', 'game'):

93 # TODO: order with core.game?

94 g = core.game.current(c)

95 if g: self._notify(g)

96

97 users = core.lobby.all_users()

98 ul = [u for u in users if core.lobby.state_of(u) == 'lobby']

99 self._notify_gamelist(ul)

100

101 return ev

102

103 def handle_game_joined(self, ev: Tuple[Game, Client]) -> Tuple[Game, Client]:

104 g, c = ev

105 Ag(self, g)['left'][c] = False

106 return ev

107

108 def handle_game_left(self, ev: Tuple[Game, Client]) -> Tuple[Game, Client]:

109 g, c = ev

110 Ag(self, g)['left'][c] = self.is_started(g)

111 return ev

112

113 def handle_game_started(self, g: Game) -> Game:

114 core = self.core

115 users = Ag(self, g)['users']

116 assert None not in users

117

118 for u in users:

119 assert core.lobby.state_of(u) == 'ready'

120

121 for u in users:

122 core.lobby.state_of(u).transit('game')

123

124 Ag(self, g)['start_time'] = time.time()

125 return g

126

127 def handle_game_ended(self, g: Game) -> Game:

128 core = self.core

129 self.games.pop(Ag(self, g)['gid'], 0)

130

131 online_users = self.online_users_of(g)

132 if not online_users:

133 return g

134

135 old = g

136

137 g = self.create_game(

138 old.__class__,

139 Ag(self, old)['name'],

140 Ag(self, old)['flags'],

141 )

142

143 for u in online_users:

144 core.lobby.state_of(u).transit('finishing')

145

146 core.events.game_successive_create.emit((old, g))

147

148 for u in Ag(self, old)['users']:

149 if self.is_online(old, u): 149 ↛ 148line 149 didn't jump to line 148, because the condition on line 149 was never false

150 self.join_game(g, u)

151

152 return old

153

154 # ----- Client Commands -----

155 @command('lobby')

156 def _create(self, u: Client, ev: wire.CreateRoom) -> None:

157 core = self.core

158 from thb import modes

159

160 if ev.mode not in modes: 160 ↛ 161line 160 didn't jump to line 161, because the condition on line 160 was never true

161 return

162

163 g = self.create_game(modes[ev.mode], ev.name, ev.flags)

164 core.invite.add_invited(g, u)

165 self.join_game(g, u, 0)

166

167 @command('lobby', 'ob')

168 def _join(self, u: Client, ev: wire.JoinRoom) -> None:

169 g = self.games.get(ev.gid)

170 if not g: 170 ↛ 171line 170 didn't jump to line 171, because the condition on line 170 was never true

171 return

172

173 log.info("join game")

174 self.join_game(g, u, ev.slot)

175

176 @command('room', 'ready', 'game')

177 def _leave(self, u: Client, ev: wire.LeaveRoom) -> None:

178 self.exit_game(u)

179

180 @command('lobby', 'room', 'ready', 'game')

181 def _users(self, u: Client, ev: wire.GetRoomUsers) -> None:

182 g = self.games.get(ev.gid)

183 if not g: 183 ↛ 184line 183 didn't jump to line 184, because the condition on line 183 was never true

184 return

185

186 self.send_room_users(g, [u])

187

188 @command('room')

189 def _set_param(self, u: Client, ev: wire.SetGameParam) -> None:

190 core = self.core

191

192 if core.lobby.state_of(u) != 'room': 192 ↛ 193line 192 didn't jump to line 193, because the condition on line 192 was never true

193 return

194

195 g = self.games.get(ev.gid)

196 if not g: 196 ↛ 197line 196 didn't jump to line 197, because the condition on line 196 was never true

197 return None

198

199 users = self.online_users_of(g)

200

201 gid = Ag(self, g)['gid']

202 if gid != ev.gid: 202 ↛ 203line 202 didn't jump to line 203, because the condition on line 202 was never true

203 log.error("Error setting game param, gid mismatch with user's current game")

204 return

205

206 cls = g.__class__

207 if ev.key not in cls.params_def:

208 log.error('Invalid option "%s"', ev.key)

209 return

210

211 if ev.value not in cls.params_def[ev.key]:

212 log.error('Invalid value "%s" for key "%s"', ev.value, ev.key)

213 return

214

215 if not core.game.set_param(g, ev.key, ev.value): 215 ↛ 216line 215 didn't jump to line 216, because the condition on line 215 was never true

216 return

217

218 for u in users:

219 if core.lobby.state_of(u) == 'ready': 219 ↛ 220line 219 didn't jump to line 220, because the condition on line 219 was never true

220 core.room.cancel_ready(u)

221

222 u.write(ev)

223 u.write(wire.GameParams(gid, core.game.params_of(g)))

224

225 @command('room')

226 def _get_ready(self, u: Client, ev: wire.GetReady) -> None:

227 self.get_ready(u)

228

229 @command('ready')

230 def _cancel_ready(self, u: Client, ev: wire.CancelReady) -> None:

231 self.cancel_ready(u)

232

233 @command('room', 'ob')

234 def _change_location(self, u: Client, ev: wire.ChangeLocation) -> None:

235 core = self.core

236

237 if core.lobby.state_of(u) not in ('room', ): 237 ↛ 238line 237 didn't jump to line 238, because the condition on line 237 was never true

238 return

239

240 core = self.core

241

242 g = core.game.current(u)

243

244 if not g: 244 ↛ 245line 244 didn't jump to line 245, because the condition on line 244 was never true

245 raise Exception('change_location called when not in game')

246

247 users = Ag(self, g)['users']

248

249 if (not 0 <= ev.loc < len(users)) or (users[ev.loc] is not self.FREESLOT):

250 return

251

252 i = users.index(u)

253 users[ev.loc], users[i] = users[i], users[ev.loc]

254

255 # core.events.game_change_location.emit(g)

256

257 # ----- Public Methods -----

258 def get_ready(self, u: Client) -> None:

259 core = self.core

260 g = core.game.current(u)

261 if not g: 261 ↛ 262line 261 didn't jump to line 262, because the condition on line 261 was never true

262 log.error('Client attempted to get ready when has no game attached: %s[%s]', u, u._)

263 return

264

265 users = Ag(self, g)['users']

266 if u not in users: 266 ↛ 267line 266 didn't jump to line 267, because the condition on line 266 was never true

267 raise Exception('WTF')

268

269 core.lobby.state_of(u).transit('ready')

270

271 if all(core.lobby.state_of(u) == 'ready' for u in users):

272 # prevent double starting

273 if not Ag(self, g)['greenlet']: 273 ↛ exitline 273 didn't return from function 'get_ready', because the condition on line 273 was never false

274 log.info("game starting")

275 game_runner = ServerGameRunner(core, g)

276

277 @game_runner.link_exception

278 def notify_crashed(runner: Any) -> None:

279 g = runner.game

280 assert g, g

281 g.ended = True

282 core.game.mark_crashed(g)

283 core.events.game_crashed.emit(g)

284 core.events.game_ended.emit(g)

285

286 @game_runner.link_value

287 def notify_ended(runner: Any) -> None:

288 g = runner.game

289 assert g, g

290 g.ended = True

291 core.events.game_ended.emit(g)

292

293 Ag(self, g)['greenlet'] = game_runner

294 core.runner.start(game_runner)

295

296 def is_online(self, g: Game, c: Client) -> bool:

297 rst = c is not self.FREESLOT

298 rst = rst and c in Ag(self, g)['users']

299 rst = rst and not Ag(self, g)['left'][c]

300 return bool(rst)

301

302 def is_left(self, g: Game, c: Client) -> bool:

303 return Ag(self, g)['left'][c]

304

305 def online_users_of(self, g: Game) -> BatchList[Client]:

306 return BatchList([u for u in Ag(self, g)['users'] if self.is_online(g, u)])

307

308 def users_of(self, g: Game) -> BatchList[Client]:

309 return Ag(self, g)['users']

310

311 def gid_of(self, g: Game) -> int:

312 return Ag(self, g)['gid']

313

314 def name_of(self, g: Game) -> str:

315 return Ag(self, g)['name']

316

317 def flags_of(self, g: Game) -> wire.msg.CreateRoomFlags:

318 return Ag(self, g)['flags']

319

320 def start_time_of(self, g: Game) -> int:

321 return int(Ag(self, g)['start_time'])

322

323 def greenlet_of(self, g: Game) -> Optional[Greenlet]:

324 return Ag(self, g)['greenlet']

325

326 def is_started(self, g: Game) -> bool:

327 return bool(Ag(self, g)['greenlet'])

328

329 def create_game(self, gamecls: type, name: str, flags: wire.msg.CreateRoomFlags) -> Game:

330 core = self.core

331 gid = self._new_gid()

332 g = core.game.create_game(gamecls)

333 self.games[gid] = g

334

335 assoc: RoomAssocOnGame = {

336 'gid': gid,

337 'users': BatchList([self.FREESLOT] * g.n_persons),

338 'left': defaultdict(bool),

339 'name': name,

340 'flags': flags, # {'match': 1, 'invite': 1}

341 'start_time': 0,

342 'greenlet': None,

343

344 '_notifier': None,

345 }

346 g._[self] = assoc

347

348 ev = core.events.game_created.emit(g)

349 assert ev

350 return g

351

352 def join_game(self, g: Game, u: Client, slot: Optional[int] = None) -> None:

353 core = self.core

354

355 assert core.lobby.state_of(u) in ('lobby', 'finishing'), core.lobby.state_of(u)

356

357 slot = slot if slot is not None else self._next_slot(g)

358

359 if slot is None: 359 ↛ 360line 359 didn't jump to line 360, because the condition on line 359 was never true

360 return

361

362 if not (0 <= slot < g.n_persons and Ag(self, g)['users'][slot] is self.FREESLOT): 362 ↛ 363line 362 didn't jump to line 363, because the condition on line 362 was never true

363 return

364

365 Ag(self, g)['users'][slot] = u

366

367 core.lobby.state_of(u).transit('room')

368 u.write(wire.GameJoined(core.view.GameDetail(g)))

369

370 core.events.game_joined.emit((g, u))

371

372 def exit_game(self, u: Client) -> None:

373 core = self.core

374

375 g = core.game.current(u)

376 if not g: 376 ↛ 377line 376 didn't jump to line 377, because the condition on line 376 was never true

377 assert core.lobby.state_of(u) not in ('room', 'ready', 'game'), core.lobby.state_of(u)

378 return

379

380 if not self.is_started(g):

381 rst = Ag(self, g)['users'].replace(u, self.FREESLOT)

382 assert rst

383

384 gid = Ag(self, g)['gid']

385

386 u.write(wire.GameLeft(Ag(self, g)['gid']))

387

388 log.info(

389 'Player %s left game [%s]',

390 core.auth.name_of(u),

391 gid,

392 )

393

394 core.lobby.state_of(u).transit('lobby')

395 core.events.game_left.emit((g, u))

396

397 if gid not in self.games: 397 ↛ 398line 397 didn't jump to line 398, because the condition on line 397 was never true

398 return

399

400 users = self.online_users_of(g)

401

402 if users:

403 return

404

405 if self.is_started(g):

406 log.info('Game [%s] aborted', gid)

407 core.game.mark_aborted(g)

408 gr = Ag(self, g)['greenlet']

409 if gr and not gr.ready(): 409 ↛ 411line 409 didn't jump to line 411, because the condition on line 409 was never false

410 gr.kill(GameAbort)

411 core.events.game_aborted.emit(g)

412 else:

413 log.info('Game [%s] cancelled', gid)

414 self.games.pop(gid, 0)

415

416 def send_room_users(self, g: Game, to: List[Client]) -> None:

417 core = self.core

418 gid = Ag(self, g)['gid']

419 pl = [core.view.User(u) for u in Ag(self, g)['users']]

420 s = Endpoint.encode(wire.RoomUsers(gid=gid, users=pl)) # former `gameinfo` and `player_change`

421 for u in to:

422 u.raw_write(s)

423

424 def cancel_ready(self, u: Client) -> None:

425 core = self.core

426 if core.lobby.state_of(u) != 'ready': 426 ↛ 427line 426 didn't jump to line 427, because the condition on line 426 was never true

427 return

428

429 g = core.game.current(u)

430 if not g: 430 ↛ 431line 430 didn't jump to line 431, because the condition on line 430 was never true

431 return

432

433 users = Ag(self, g)['users']

434 if u not in users: 434 ↛ 435line 434 didn't jump to line 435, because the condition on line 434 was never true

435 log.error('User not in player list')

436 return

437

438 core.lobby.state_of(u).transit('room')

439

440 def get(self, gid: int) -> Optional[Game]:

441 return self.games.get(gid)

442

443 # ----- Methods -----

444 def _init_freeslot(self) -> None:

445 core = self.core

446 self.FREESLOT = u = Client(core, None)

447 core.auth.set_auth(u, uid=0, name='空位置')

448 core.lobby.init_freeslot(u)

449 core.lobby.state_of(u).transit('freeslot')

450

451 def _notify(self, g: Game) -> None:

452 core = self.core

453 notifier = Ag(self, g)['_notifier']

454

455 if notifier:

456 notifier()

457 return

458

459 @throttle(0.5)

460 def _notifier() -> None:

461 core.runner.spawn(self.send_room_users, g, Ag(self, g)['users'])

462

463 Ag(self, g)['_notifier'] = _notifier

464 _notifier()

465

466 @throttle(3)

467 def _notify_gamelist(self, ul: List[Client]) -> None:

468 core = self.core

469

470 lst = [core.view.Game(g) for g in self.games.values()]

471 d = Endpoint.encode_bulk([wire.CurrentGames(lst)])

472

473 @core.runner.spawn

474 def do_send() -> None:

475 for u in ul:

476 u.raw_write(d)

477

478 def _next_slot(self, g: Game) -> Optional[int]:

479 try:

480 return Ag(self, g)['users'].index(self.FREESLOT)

481 except ValueError:

482 return None

483

484 def _new_gid(self) -> int:

485 core = self.core

486 gid = core.backend.query('query { gameId }')['gameId']

487 return gid