Guest User

Untitled

a guest
Jan 31st, 2018
92
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 17.46 KB | None | 0 0
  1. import importlib
  2. import os
  3. import string
  4. from itertools import cycle
  5.  
  6. from discord.ext import commands
  7.  
  8. from .examples.example_game import GameBoardTicTacToe, PlayerTicTacToe, \
  9. is_move_valid, \
  10. make_move, get_messages, check_win, cleanup_game, \
  11. pick_random_mark
  12. from .important_types import WinCode, DrawCode, UserID, RoleID, GameTypeInfo, LiveGameInfo
  13.  
  14. # typechecking stuff
  15. from .gamesession import GameSession
  16. from discord.ext.commands import Context
  17. from discord import Member
  18. from typing import List, Dict, Union, Iterable, Any, Callable, TypeVar, Sequence, Type, Tuple, Optional, cast, NewType, Set
  19. from . import games
  20. # ---
  21.  
  22.  
  23. class DiscordGamesCogError(Exception):
  24. pass
  25.  
  26.  
  27. class InvalidGameInfoError(DiscordGamesCogError):
  28. pass
  29.  
  30.  
  31. class DiscordGamesCog:
  32. def __init__(self, bot:Any) -> None:
  33. self.gameadmin_role_id = RoleID("403127699134218250")
  34. self.bot = bot
  35. self._games_filenames = self._load_games_filenames() # type: List[str]
  36. self.games = {} # type: Dict[str, GameTypeInfo]
  37. self.live_games = {} # type: Dict[str, LiveGameInfo]
  38.  
  39. self.invalid_gamename_chars = {*string.punctuation, *string.whitespace, *string.printable} # type: Set[str]
  40. self.max_gamename_length = 10
  41.  
  42. # Loading games stuff: START #
  43. # ======================================================================== #
  44.  
  45. def _load_games_filenames(self) -> List[str]:
  46. # Finds .py files in ./games/ for use
  47. return games.get_mod_names()
  48.  
  49. def _is_member_gameadmin(self, member:Member) -> bool:
  50. return any(RoleID(role.id) == self.gameadmin_role_id for role in member.roles)
  51.  
  52. def _is_game_type_live(self, game_type:str) -> bool:
  53. for game_info in self.live_games.values():
  54. if game_info["game_type"] == game_type:
  55. return True
  56. return False
  57.  
  58. def _import_helper(self, game_type:str, package:Optional[str]=None) -> None:
  59. mod_obj = importlib.import_module(game_type, package) # type: Any
  60. new_gametypeinfo = mod_obj.get_gametypeinfo() # type: GameTypeInfo
  61. self.games[game_type] = new_gametypeinfo
  62.  
  63. # Reload game | Load game | Unload game
  64. @commands.command(pass_context=True)
  65. async def reloadgame(self, ctx:Context, game_type:str) -> None:
  66. if self._is_member_gameadmin(ctx.message.author):
  67. if game_type not in self.games:
  68. await self.bot.say("Game {} hasn't been loaded yet!".format(game_type))
  69. elif self._is_game_type_live(game_type):
  70. await self.bot.say("Game {} has game sessions in progress!".format(game_type))
  71. else:
  72. self._import_helper(game_type, package="discord_games.games")
  73. await self.bot.say("Game {} reloaded.".format(game_type))
  74. else:
  75. await self.bot.say("You don't have permission to do that!")
  76.  
  77. @commands.command(pass_context=True)
  78. async def loadgame(self, ctx:Context, game_type:str) -> None:
  79. if self._is_member_gameadmin(ctx.message.author):
  80. if game_type not in self._games_filenames:
  81. await self.bot.say("Game {} is not a supported game!".format(game_type))
  82. elif game_type in self.games:
  83. await self.bot.say("Game {} is already loaded!".format(game_type))
  84. else:
  85. self._import_helper(game_type, package="discord_games.games")
  86. await self.bot.say("Game {} loaded.".format(game_type))
  87. else:
  88. await self.bot.say("You don't have permission to do that!")
  89.  
  90. @commands.command(pass_context=True)
  91. async def unloadgame(self, ctx:Context, game_type:str) -> None:
  92. if self._is_member_gameadmin(ctx.message.author):
  93. if game_type not in self.games:
  94. await self.bot.say("Game {} hasn't been loaded yet!".format(game_type))
  95. elif self._is_game_type_live(game_type):
  96. await self.bot.say("Game {} has game sessions in progress!".format(game_type))
  97. else:
  98. del self.games[game_type]
  99. await self.bot.say("Game {} unloaded.".format(game_type))
  100. else:
  101. await self.bot.say("You don't have permission to do that!")
  102.  
  103. # Reload all games | Load all games | Unload all games
  104. @commands.command(pass_context=True)
  105. async def reloadallgames(self, ctx:Context) -> None:
  106. if self._is_member_gameadmin(ctx.message.author):
  107. if len(self.live_games) != 0:
  108. # Unloading
  109. await self.bot.say("Unloading games...")
  110. for game_type in self.games.keys():
  111. del self.games[game_type]
  112. await self.bot.say("Games unloaded.")
  113.  
  114. # Reloading
  115. await self.bot.say("Loading games...")
  116. for filename in self._games_filenames:
  117. self._import_helper(filename, package="discord_games.games")
  118. await self.bot.say("All games reloaded.")
  119. else:
  120. await self.bot.say("You can't reload all games if any games are live!")
  121. else:
  122. await self.bot.say("You don't have permission to do that!")
  123.  
  124. @commands.command(pass_context=True)
  125. async def loadallgames(self, ctx:Context) -> None:
  126. if self._is_member_gameadmin(ctx.message.author):
  127. for filename in self._games_filenames:
  128. self._import_helper(filename, package="discord_games.games")
  129. await self.bot.say("All games loaded.")
  130. else:
  131. await self.bot.say("You don't have permission to do that!")
  132.  
  133. @commands.command(pass_context=True)
  134. async def unloadallgames(self, ctx:Context) -> None:
  135. if self._is_member_gameadmin(ctx.message.author):
  136. if len(self.live_games) != 0:
  137. for game_type in self.games.keys():
  138. del self.games[game_type]
  139. await self.bot.say("All games unloaded.")
  140. else:
  141. await self.bot.say("You can't reload all games if any games are live!")
  142. else:
  143. await self.bot.say("You don't have permission to do that!")
  144.  
  145. # Loading games stuff: End #
  146. # ======================================================================== #
  147.  
  148. # The following commands only show the user information
  149. @commands.command(pass_context=False)
  150. async def showgames(self) -> None:
  151. lines = []
  152. lines.append("```")
  153. lines.append("Available games:")
  154. for gamename in self.games.keys():
  155. lines.append("\t * {}".format(gamename))
  156. lines.append("```")
  157. await self.bot.say("\n".join(lines))
  158.  
  159. @commands.command(pass_context=False)
  160. async def showlobbies(self) -> None:
  161. lines = []
  162. lines.append("```")
  163. lines.append("Un-started lobbies:")
  164. for lobby_name, lobby_info in self.live_games.items():
  165. num_players = len(lobby_info["session"].players)
  166. max_players = lobby_info["max_players"]
  167. started = lobby_info["started"]
  168. lines.append("\t{lobby_name}: {num_players}/{max_players} players | started?: {started}".format(**locals()))
  169. lines.append("```")
  170. await self.bot.say("\n".join(lines))
  171.  
  172. # Actual game stuff: Start #
  173. # ======================================================================== #
  174.  
  175. # Helper functions to shorten functions where they're used in + to avoid duplicating code
  176. def _call_callables_in_args(self, args:Iterable[Any]) -> List[Any]:
  177. return [arg() if callable(arg) else arg for arg in args]
  178.  
  179. def _call_callables_in_kwargs(self, kwargs:Dict[str, Any]) -> Dict[str, Any]:
  180. return {key:val() if callable(val) else val for key, val in kwargs.items()}
  181.  
  182. def _get_invalid_chars(self, chars:str) -> str:
  183. invalid_chars_found = set()
  184. for char in chars:
  185. if char in self.invalid_gamename_chars:
  186. invalid_chars_found.add(char)
  187. return ", ".join(invalid_chars_found)
  188.  
  189. def _is_gamename_valid(self, name:str) -> bool:
  190. return not any(char in self.invalid_gamename_chars for char in name)
  191.  
  192. @commands.command(pass_context=True)
  193. async def newgame(self, ctx:Context, game_type:str, gamename:str) -> None:
  194. try:
  195. new_game_info = self.games[game_type] # type: GameTypeInfo
  196. except KeyError:
  197. await self.bot.say("{} is not a supported game!".format(gamename))
  198. else:
  199. if gamename in self.live_games:
  200. await self.bot.say("Gamesession with name '{}' is already taken!".format(gamename))
  201. elif self._is_gamename_valid(gamename):
  202. await self.bot.say("Gamesession '{}' has invalid chars '{}'".format(gamename, self._get_invalid_chars(gamename)))
  203. elif len(gamename) > self.max_gamename_length:
  204. await self.bot.say("Gamesession requested by {} has too long of a name".format(ctx.message.author))
  205. else:
  206. # Validity checks passed
  207. # Instantiating the `field_of_play` for the new game session
  208. field_of_play_info = new_game_info["field_of_play"]
  209.  
  210. # Making sure any "saved" function calls in args and kwargs get called
  211. field_args = field_of_play_info["args"]
  212. field_kwargs = field_of_play_info["kwargs"]
  213.  
  214. new_field_args = self._call_callables_in_args(field_args)
  215. new_field_kwargs = self._call_callables_in_kwargs(field_kwargs)
  216.  
  217. new_field_of_play = field_of_play_info["cls"](*new_field_args, **new_field_kwargs)
  218.  
  219. # Getting the other arguments to instantiate a new `GameSession`
  220. is_move_valid_func = new_game_info["is_move_valid_func"]
  221. make_move_func = new_game_info["make_move_func"]
  222. get_messages_func = new_game_info["get_messages_func"]
  223. win_checker_func = new_game_info["win_checker_func"]
  224. cleanup_func = new_game_info["cleanup_func"]
  225.  
  226. self.live_games[gamename] = {
  227. "game_type": game_type,
  228. "max_players": self.games[game_type]["max_players"],
  229. "session": GameSession(new_field_of_play, is_move_valid_func, make_move_func, get_messages_func, win_checker_func, cleanup_func),
  230. "owner": ctx.message.author,
  231. "started": False
  232. }
  233.  
  234. # Adding the creator of the new game session (to the game session)
  235. new_player_info = new_game_info["player"]
  236. new_player_cls = new_player_info["cls"]
  237.  
  238. # Making sure any "saved" function calls in args and kwargs get called
  239. args = new_player_info["args"]
  240. kwargs = new_player_info["kwargs"]
  241.  
  242. new_args = self._call_callables_in_args(args)
  243. new_kwargs = self._call_callables_in_kwargs(kwargs)
  244.  
  245. game_session = self.live_games[gamename]["session"]
  246. game_session.add_player(ctx.message.author, new_player_cls, new_args, new_kwargs)
  247. await self.bot.say("Gamesession '{}' created".format(gamename))
  248.  
  249. @commands.command(pass_context=True)
  250. async def editgame(self, ctx:Context, gamename:str, edits:str) -> None:
  251. if gamename not in self.live_games:
  252. await self.bot.say("Gamesession '{}' is not a live game!".format(gamename))
  253. elif ctx.message.author != self.live_games[gamename]["owner"]:
  254. await self.bot.say("You're not the owner of this gamesession {}!".format(ctx.message.author))
  255. else:
  256. # Validity checks passed
  257. # ... # Here you would edit the settings (if any) using `edits`, as well as checking `edits` for validity
  258. await self.bot.say("This doesn't do anything yet!")
  259.  
  260. @commands.command(pass_context=True)
  261. async def joingame(self, ctx:Context, gamename:str) -> None:
  262. if gamename not in self.live_games:
  263. await self.bot.say("Gamesession '{}' is not a live game!".format(gamename))
  264. elif ctx.message.author in self.live_games[gamename]["session"].players:
  265. await self.bot.say("You're already in the game! ({})".format(gamename))
  266. elif len(self.live_games[gamename]["session"].players) == self.live_games[gamename]["max_players"]:
  267. await self.bot.say("Gamesession '{}' is full.".format(gamename))
  268. else:
  269. # Validity checks passed
  270. # Adding the new player to the game session
  271. player_info = self.games[self.live_games[gamename]["game_type"]]["player"] # Info about how to create new player objects for this game
  272.  
  273. new_player_cls = player_info["cls"]
  274.  
  275. # Making sure any "saved" function calls in args and kwargs get called
  276. args = player_info["args"]
  277. kwargs = player_info["kwargs"]
  278.  
  279. new_args = self._call_callables_in_args(args)
  280. new_kwargs = self._call_callables_in_kwargs(kwargs)
  281.  
  282. self.live_games[gamename]["session"].add_player(ctx.message.author, new_player_cls, new_args, new_kwargs)
  283.  
  284. @commands.command(pass_context=True)
  285. async def startgame(self, ctx:Context, gamename:str) -> None:
  286. if gamename not in self.live_games:
  287. await self.bot.say("Gamesession '{}' is not a live game!".format(gamename))
  288. elif ctx.message.author != self.live_games[gamename]["owner"]:
  289. await self.bot.say("You're not the owner of this gamesession {}!".format(ctx.message.author))
  290. else:
  291. # Validity checks passed
  292. # Rewrite this mess
  293. game_info = self.games[self.live_games[gamename]["game_type"]]
  294.  
  295. # Generate a turn order and set the first player turn (`current_player_turn`) to the first value yielded by `game_session.turn_order`
  296. game_session = self.live_games[gamename]["session"]
  297. game_session.turn_order = cycle(game_info["choose_turn_order_func"](game_session.players.keys()))
  298. game_session.current_player_turn = next(game_session.turn_order)
  299.  
  300. self.live_games[gamename]["started"] = True # Let the function that handles commands for moves know that it should accept moves
  301.  
  302. messages_to_say = game_session.get_messages(DrawCode.start_of_game)
  303. for msg in messages_to_say:
  304. await self.bot.say(msg)
  305.  
  306. @commands.command(pass_context=True)
  307. async def play(self, ctx:Context, gamename:str, move_name:str, *args:str) -> None:
  308. if gamename not in self.live_games:
  309. await self.bot.say("Gamesession '{}' is not a live game!".format(gamename))
  310. elif ctx.message.author not in self.live_games[gamename]["session"].players:
  311. await self.bot.say("You're not in this gamesession {}!".format(ctx.message.author))
  312. elif not self.live_games[gamename]["started"]:
  313. await self.bot.say("Gamesession '{}' has not started yet!".format(gamename))
  314. elif move_name not in self.games[self.live_games[gamename]["game_type"]]:
  315. await self.bot.say("Move named '{}' is not a supported move for this game!".format(move_name))
  316. else:
  317. # Validity checks passed
  318. game_session = self.live_games[gamename]["session"]
  319. move_is_valid, reason_for_invalid = game_session.is_move_valid(move_name, args)
  320. if ctx.message.author == game_session.current_player_turn:
  321. if move_is_valid:
  322. messages_to_say = game_session.make_move(move_name, args)
  323. if messages_to_say is not None:
  324. for msg in messages_to_say:
  325. await self.bot.say(msg)
  326.  
  327. win_code, winners = game_session.check_win()
  328. if win_code == WinCode.win:
  329. messages_to_say = game_session.cleanup_game(winners)
  330. for msg in messages_to_say:
  331. await self.bot.say(msg)
  332. elif win_code == WinCode.draw:
  333. messages_to_say = game_session.get_messages(DrawCode.game_draw)
  334. for msg in messages_to_say:
  335. await self.bot.say(msg)
  336. else: # has_won == WinCode.no_win
  337. messages_to_say = game_session.get_messages(DrawCode.no_win)
  338. for msg in messages_to_say:
  339. await self.bot.say(msg)
  340. else:
  341. await self.bot.say("Move is not valid for reason:\n\t{}".format(reason_for_invalid))
  342. else:
  343. await self.bot.say("It's not your turn {}!".format(ctx.message.author))
  344.  
  345. # Actual game stuff: End #
  346. # ======================================================================== #
  347.  
  348. # Utility stuff for testing: START: #
  349. # ======================================================================== #
  350.  
  351. @commands.command(pass_context=True)
  352. async def showinternals(self, ctx:Context) -> None:
  353. if self._is_member_gameadmin(ctx.message.author):
  354. lines = []
  355. lines.append("```")
  356. lines.append("My internals:")
  357. for attr in vars(self):
  358. lines.append("\t{}={}".format(attr, self.__dict__[attr]))
  359. lines.append("```")
  360. msg = "\n".join(lines)
  361. await self.bot.say(msg)
  362. else:
  363. await self.bot.say("You don't have permission to do that!")
  364.  
  365. # Utility stuff for testing: END: #
  366. # ======================================================================== #
Add Comment
Please, Sign In to add comment