cgame.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. #!/usr/bin/env python
  2. """
  3. Class defenition for Carcassonne score keeping system.
  4. Copyright 2018 George C. Privon
  5. This program is free software: you can redistribute it and/or modify
  6. it under the terms of the GNU General Public License as published by
  7. the Free Software Foundation, either version 3 of the License, or
  8. (at your option) any later version.
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU General Public License for more details.
  13. You should have received a copy of the GNU General Public License
  14. along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. """
  16. import re as _re
  17. import sys as _sys
  18. from datetime import datetime as _datetime
  19. import sqlite3 as _sqlite3
  20. import numpy as _np
  21. class cgame:
  22. """
  23. Carcassonne game object
  24. """
  25. def __init__(self):
  26. """
  27. Initialize some variables and set up a game
  28. """
  29. self.commands = [('r', 'record score and advance turn'),
  30. ('t', 'advance turn, no score'),
  31. ('b', 'additional turn for a player due to a builder (use for the 2nd play by a player)'),
  32. ('e', 'end game (or play if already in postgame scoring'),
  33. ('s', '(current) score and game status'),
  34. ('?', 'print help')]
  35. self.conn = _sqlite3.connect('CarcassonneScore.db')
  36. self.cur = self.conn.cursor()
  37. self.setupGame()
  38. def showCommands(self):
  39. """
  40. Print out a list of valid commands for in-game play.
  41. """
  42. _sys.stderr.write('Possible commands:\n')
  43. for entry in self.commands:
  44. _sys.stderr.write('\t' + entry[0] + ': ' + entry[1] + '\n')
  45. def setupGame(self):
  46. """
  47. Initialize a game
  48. """
  49. # game state information
  50. self.state = 0 # 0 for main game, 1 for postgame, 2 for ended game
  51. self.ntile = 0 # number of tiles played
  52. self.nbuilder = 0 # number of tiles placed due to builders
  53. self.totaltiles = 72 # may be increased by expansions
  54. # get players for this game
  55. _sys.stdout.write("Collecting player information...\n")
  56. while self.getPlayers():
  57. continue
  58. # get expansions used for this game
  59. _sys.stdout.write("Collecting expansion information...\n")
  60. while self.getExpansions():
  61. continue
  62. # get general game info (do this after expansions because
  63. # expansion info is entered into the game table)
  64. while self.gameInfo():
  65. continue
  66. def gameInfo(self):
  67. """
  68. Load basic game info
  69. """
  70. location = input("Where is the game being played? ")
  71. starttime = _datetime.utcnow().strftime("%Y-%m-%dT%H:%M")
  72. self.cur.execute('INSERT INTO games (location, starttime, expansions) VALUES ("' + location + '","' + starttime + '","' + ','.join(["{0:d}".format(x) for x in self.expansionIDs]) + '")')
  73. gID = self.cur.execute('select last_insert_rowid();').fetchall()[0]
  74. self.gameID = gID[0]
  75. def getPlayers(self):
  76. """
  77. Get a list of possible players from the database
  78. """
  79. self.players = []
  80. dbplayers = self.cur.execute('''SELECT * FROM players''').fetchall()
  81. if len(dbplayers):
  82. for dbplayer in dbplayers:
  83. _sys.stdout.write("{0:d}) ".format(dbplayer[0]) + dbplayer[1] + '\n')
  84. playerinput = input("Please list the IDs for the players in this game (in order of play): ")
  85. playerIDs = [int(x) for x in playerinput.split()]
  86. for playerID in playerIDs:
  87. matched = False
  88. for dbplayer in dbplayers:
  89. if playerID == dbplayer[0]:
  90. self.players.append((playerID, dbplayer[1]))
  91. matched = True
  92. continue
  93. if not matched:
  94. _sys.stderr.write("Error: player ID {0:d} does not match an option from the list.\n".format(playerID))
  95. return 1
  96. else:
  97. _sys.stderr.write("Error: players table empty. Exiting.\n")
  98. _sys.exit(-1)
  99. return 0
  100. def getExpansions(self):
  101. """
  102. Get a list of playable expansions
  103. """
  104. self.expansionIDs = []
  105. self.tokens = ["Meeple"]
  106. self.tiletypes = []
  107. for minisel in range(0, 2):
  108. if minisel:
  109. exptype = "mini"
  110. else:
  111. exptype = "large"
  112. dbexpans = self.cur.execute('''SELECT expansionID,name,tokens,Ntiles,tiletypes FROM expansions WHERE active==1 and mini=={0:d}'''.format(minisel)).fetchall()
  113. if len(dbexpans):
  114. for dbexpan in dbexpans:
  115. _sys.stdout.write("{0:d}) ".format(dbexpan[0]) + dbexpan[1] + '\n')
  116. expaninput = input("Please list the numbers for the " + exptype + " used in this game: ")
  117. expanIDs = [int(x) for x in expaninput.split()]
  118. for expanID in expanIDs:
  119. matched = False
  120. for dbexpan in dbexpans:
  121. if expanID == dbexpan[0]:
  122. self.expansionIDs.append(expanID)
  123. self.totaltiles += dbexpan[3]
  124. ttypes = dbexpan[2].split(',')
  125. if len(ttypes):
  126. for token in ttypes:
  127. self.tokens.append(token)
  128. tiletypes = dbexpan[4].split(',')
  129. if len(tiletypes):
  130. for tile in tiletypes:
  131. self.tiletypes.append(tile)
  132. matched = True
  133. continue
  134. if not matched:
  135. _sys.stderr.write("Error: expansion ID {0:d} does not match an option from the list.\n".format(expanID))
  136. return 1
  137. else:
  138. _sys.stdout.write("No active " + exptype + " expansions found. Continuing.\n")
  139. return 0
  140. def recordScore(self):
  141. """
  142. Record a score event in the game
  143. """
  144. if self.state:
  145. ingame = 0
  146. player = self.getCurrentPlayer()
  147. return 0
  148. def advanceTurn(self, builder=False):
  149. """
  150. Make a new entry in the turns table
  151. """
  152. cmdtime = _datetime.utcnow().strftime("%Y-%m-%dT%H:%M")
  153. command = '''INSERT INTO turns VALUES ({0:d}, {1:d}, "'''.format(self.gameID, self.ntile)
  154. command = command + cmdtime + '"'
  155. if builder:
  156. bID = 1
  157. else:
  158. bID = 0
  159. # compute playerID based on the turn number minus nbuilders / number of players
  160. player = self.getCurrentPlayer()
  161. command = command + ', {0:d}, {1:d})'.format(bID, player[0])
  162. self.cur.execute(command)
  163. self.ntile += 1
  164. if builder:
  165. self.nbuilder += 1
  166. def runGame(self):
  167. """
  168. Main routine for entering games
  169. """
  170. # here wait for input for scores, advancing to next round, or completion of game
  171. # for each step of entry, present a series of options, based on the list
  172. # of playerIDs and expansions
  173. while self.state < 2:
  174. # set up prompt based on current round
  175. if self.state:
  176. prompt = "postgame > "
  177. else:
  178. player = self.getCurrentPlayer()
  179. prompt = "round: {0:d}, turn: {1:d} ".format(int(_np.floor((self.ntile-self.nbuilder) / len(self.players))),
  180. self.ntile-self.nbuilder)
  181. prompt = prompt + "(" + player[1] + ") > "
  182. try:
  183. cmd = input(prompt)
  184. except (EOFError, KeyboardInterrupt):
  185. _sys.stderr.write('Improper input. Please retry\n')
  186. self.showCommands()
  187. if _re.match('e', cmd, _re.IGNORECASE):
  188. self.advanceState()
  189. elif _re.match('s', cmd, _re.IGNORECASE):
  190. self.printStatus(tilestats=True)
  191. elif _re.match('n', cmd, _re.IGNORECASE):
  192. self.advanceTurn()
  193. elif _re.match('r', cmd, _re.IGNORECASE):
  194. self.recordScore()
  195. elif _re.match('t', cmd, _re.IGNORECASE):
  196. self.advanceTurn(builder=False)
  197. elif _re.match('b', cmd, _re.IGNORECASE):
  198. self.advanceTurn(builder=True)
  199. elif _re.match('\?', cmd, _re.IGNORECASE):
  200. self.showCommands()
  201. else:
  202. _sys.stderr.write('Command not understood. Please try again.\n')
  203. self.showCommands()
  204. if state == 2:
  205. #game is over. write end time to the games table
  206. time = _datetime.utcnow().strftime("%Y-%m-%dT%H:%M")
  207. self.cur.execute('''UPDATE games SET endtime = "''' + time + '''" WHERE gameID = ''' + str(gameID))
  208. conn.commit()
  209. printStatus(tilestats=False)
  210. #### Is there a way to capture "ineffective" uses? For example,
  211. #### meeples that don't score points because they end up in a meadow that's
  212. #### controled by someone else?
  213. return 0
  214. def printStatus(self, tilestats=False):
  215. """
  216. Print the total score (current or final) for the specified gameID
  217. """
  218. _sys.stdout.write('\nCurrent Score\n')
  219. for player in self.players:
  220. a = self.cur.execute('SELECT points FROM scores WHERE gameID={0:d} and playerID={1:d}'.format(self.gameID, player[0]))
  221. res = a.fetchall()
  222. score = _np.sum(res)
  223. _sys.stdout.write('\t' + player[1]+ ': {0:1.0f}'.format(score) + '\n')
  224. _sys.stdout.write("{0:1.0f} tiles played, {1:1.0f} remaining.\n\n".format(self.ntile,
  225. self.totaltiles - self.ntile))
  226. def getCurrentPlayer(self):
  227. """
  228. Return the current player, determined by the turn number
  229. """
  230. return self.players[int((self.ntile - self.nbuilder) % len(self.players))]