cgame.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  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'),
  30. ('n', 'next turn'),
  31. ('e', 'end game (or end play if already in postgame scoring)'),
  32. ('s', '(current) score and game status'),
  33. ('q', 'quit (will be removed for real gameplay'),
  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.nscore = 0
  52. self.ntile = 0 # number of tiles played
  53. self.nbuilder = 0 # number of tiles placed due to builders
  54. self.totaltiles = 72 # may be increased by expansions
  55. # get players for this game
  56. _sys.stdout.write("Collecting player information...\n")
  57. while self.getPlayers():
  58. continue
  59. # get expansions used for this game
  60. _sys.stdout.write("Collecting expansion information...\n")
  61. while self.getExpansions():
  62. continue
  63. # get general game info (do this after expansions because
  64. # expansion info is entered into the game table)
  65. while self.gameInfo():
  66. continue
  67. def gameInfo(self):
  68. """
  69. Load basic game info
  70. """
  71. location = input("Where is the game being played? ")
  72. starttime = _datetime.utcnow().strftime("%Y-%m-%dT%H:%M")
  73. self.cur.execute('INSERT INTO games (location, starttime, expansions) VALUES ("' + location + '","' + starttime + '","' + ','.join(["{0:d}".format(x) for x in self.expansionIDs]) + '")')
  74. gID = self.cur.execute('select last_insert_rowid();').fetchall()[0]
  75. self.conn.commit()
  76. self.gameID = gID[0]
  77. self.tokens = ["Meeple"]
  78. self.tiletypes = []
  79. self.scoretypes = ["Meadow", "City", "Road"]
  80. def getPlayers(self):
  81. """
  82. Get a list of possible players from the database
  83. """
  84. self.players = []
  85. dbplayers = self.cur.execute('''SELECT * FROM players''').fetchall()
  86. if len(dbplayers):
  87. for dbplayer in dbplayers:
  88. _sys.stdout.write("{0:d}) ".format(dbplayer[0]) + dbplayer[1] + '\n')
  89. playerinput = input("Please list the IDs for the players in this game (in order of play): ")
  90. playerIDs = [int(x) for x in playerinput.split()]
  91. for playerID in playerIDs:
  92. matched = False
  93. for dbplayer in dbplayers:
  94. if playerID == dbplayer[0]:
  95. self.players.append((playerID, dbplayer[1]))
  96. matched = True
  97. continue
  98. if not matched:
  99. _sys.stderr.write("Error: player ID {0:d} does not match an option from the list.\n".format(playerID))
  100. return 1
  101. else:
  102. _sys.stderr.write("Error: players table empty. Exiting.\n")
  103. _sys.exit(-1)
  104. return 0
  105. def getExpansions(self):
  106. """
  107. Get a list of playable expansions
  108. """
  109. self.expansionIDs = []
  110. for minisel in range(0, 2):
  111. if minisel:
  112. exptype = "mini"
  113. else:
  114. exptype = "large"
  115. dbexpans = self.cur.execute('''SELECT expansionID,name,tokens,Ntiles,tiletypes,scoretypes FROM expansions WHERE active==1 and mini=={0:d}'''.format(minisel)).fetchall()
  116. if len(dbexpans):
  117. for dbexpan in dbexpans:
  118. _sys.stdout.write("{0:d}) ".format(dbexpan[0]) + dbexpan[1] + '\n')
  119. expaninput = input("Please list the numbers for the " + exptype + " used in this game: ")
  120. expanIDs = [int(x) for x in expaninput.split()]
  121. for expanID in expanIDs:
  122. matched = False
  123. # add the builder cmd if Traders & Builders is used
  124. if expanID == 2:
  125. self.commands.append(('b', 'additional turn for a player due to a builder (use for the 2nd play by a player)'))
  126. for dbexpan in dbexpans:
  127. if expanID == dbexpan[0]:
  128. self.expansionIDs.append(expanID)
  129. self.totaltiles += dbexpan[3]
  130. ttypes = dbexpan[2].split(',')
  131. if len(ttypes):
  132. # add new types of tokens
  133. for token in ttypes:
  134. self.tokens.append(token)
  135. tiletypes = dbexpan[4].split(',')
  136. if len(tiletypes):
  137. # add special tiles
  138. for tile in tiletypes:
  139. self.tiletypes.append(tile)
  140. stypes = dbexpan[5].split(',')
  141. if len(stypes):
  142. # add new types of scoring
  143. for stype in stypes:
  144. self.scoretypes.append(stype)
  145. matched = True
  146. continue
  147. if not matched:
  148. _sys.stderr.write("Error: expansion ID {0:d} does not match an option from the list.\n".format(expanID))
  149. return 1
  150. else:
  151. _sys.stdout.write("No active " + exptype + " expansions found. Continuing.\n")
  152. return 0
  153. def recordScore(self):
  154. """
  155. Record a score event in the game
  156. """
  157. score = {'gameID': self.gameID,
  158. 'playerIDs': -1,
  159. 'turnNum': self.ntile,
  160. 'scoreID': self.nscore,
  161. 'ingame' : 1,
  162. 'points' : 0,
  163. 'scoretype': '',
  164. 'sharedscore': 0,
  165. 'token': '',
  166. 'extras': '',
  167. 'comments': ''}
  168. if self.state:
  169. score['ingame'] = 0
  170. # ask the user which player scored
  171. VALID = False
  172. while not VALID:
  173. for player in self.players:
  174. _sys.stdout.write("{0:d}) ".format(player[0]) + player[1] + "\n")
  175. scoreplayers = input("Please enter the numbers for the players who scored: ")
  176. score['playerIDs'] = [int(x) for x in scoreplayers.split()]
  177. VALID = True
  178. if len(score['playerIDs']) > 1:
  179. score['sharedscore'] = 1
  180. # get points for score
  181. VALID = False
  182. while not VALID:
  183. points = input("Enter the total number of points: ")
  184. try:
  185. score['points'] = int(points)
  186. VALID = True
  187. except:
  188. _sys.stderr.write("'" + points + "' is not a valid score.\n")
  189. continue
  190. # get the score type
  191. VALID = False
  192. while not VALID:
  193. for i, stype in enumerate(self.scoretypes):
  194. _sys.stdout.write("{0:d}) ".format(i+1) + stype + "\n")
  195. # here i want a list of valid score types
  196. stype = input("Please select the score type: ")
  197. try:
  198. score['scoretype'] = self.scoretypes[int(stype)-1]
  199. VALID = True
  200. except:
  201. _sys.stderr.write("'" + stype + "' is not a valid score type.\n")
  202. continue
  203. # see which token scored
  204. # really this should be expanded to allow multiple token types for one score
  205. if len(self.tokens) > 1:
  206. VALID = False
  207. while not VALID:
  208. for i, token in enumerate(self.tokens):
  209. _sys.stdout.write("{0:d}) ".format(i+1) + token + "\n")
  210. tID = input("Please select the token type: ")
  211. try:
  212. score['token'] += self.tokens[int(tID-1)]
  213. VALID = True
  214. except:
  215. _sys.stderr.write("'" + command + "' is not a valid token.\n")
  216. continue
  217. else:
  218. score['token'] = self.tokens[0]
  219. # shared score?
  220. VALID = False
  221. while not VALID:
  222. shared = input("Was this score shared with another player (y/n)? ")
  223. if _re.match('y', shared, _re.IGNORECASE):
  224. score['sharedscore'] = 1
  225. VALID = True
  226. elif _re.match('n', shared, _re.IGNORECASE):
  227. VALID = True
  228. else:
  229. _sys.stderr.write("Invalid input.\n")
  230. # now construct a SQL query
  231. command = 'INSERT INTO scores VALUE ({0:d},'.format(self.gameID)
  232. command = command + '{0:d}, {1:d},'.format(self.ntile,
  233. self.nscore)
  234. # if score['sharedscore']:
  235. # get the other player(s) who scored and construct SQL inserts for
  236. # scores
  237. # now increment the score number
  238. self.nscore += 1
  239. return 0
  240. def advanceTurn(self, builder=False):
  241. """
  242. Make a new entry in the turns table
  243. """
  244. cmdtime = _datetime.utcnow().strftime("%Y-%m-%dT%H:%M")
  245. command = '''INSERT INTO turns VALUES ({0:d}, {1:d}, "'''.format(self.gameID, self.ntile)
  246. command = command + cmdtime + '"'
  247. if builder:
  248. bID = 1
  249. else:
  250. bID = 0
  251. # compute playerID based on the turn number minus nbuilders / number of players
  252. player = self.getCurrentPlayer()
  253. command = command + ', {0:d}, {1:d})'.format(bID, player[0])
  254. self.cur.execute(command)
  255. self.conn.commit()
  256. self.ntile += 1
  257. if builder:
  258. self.nbuilder += 1
  259. def runGame(self):
  260. """
  261. Main routine for entering games
  262. """
  263. # here wait for input for scores, advancing to next round, or completion of game
  264. # for each step of entry, present a series of options, based on the list
  265. # of playerIDs and expansions
  266. while self.state < 2:
  267. # set up prompt based on current round
  268. if self.state:
  269. prompt = "postgame > "
  270. else:
  271. player = self.getCurrentPlayer()
  272. prompt = "round: {0:d}, turn: {1:d} ".format(int(_np.floor((self.ntile-self.nbuilder) / len(self.players))),
  273. self.ntile-self.nbuilder)
  274. prompt = prompt + "(" + player[1] + ") > "
  275. try:
  276. cmd = input(prompt)
  277. except (EOFError, KeyboardInterrupt):
  278. _sys.stderr.write('Improper input. Please retry\n')
  279. self.showCommands()
  280. if _re.match('e', cmd, _re.IGNORECASE):
  281. self.advanceState()
  282. elif _re.match('q', cmd, _re.IGNORECASE):
  283. _sys.exit(0)
  284. elif _re.match('s', cmd, _re.IGNORECASE):
  285. self.printStatus(tilestats=True)
  286. elif _re.match('n', cmd, _re.IGNORECASE):
  287. self.advanceTurn(builder=False)
  288. elif _re.match('r', cmd, _re.IGNORECASE):
  289. self.recordScore()
  290. elif _re.match('b', cmd, _re.IGNORECASE):
  291. self.advanceTurn(builder=True)
  292. elif _re.match('\?', cmd, _re.IGNORECASE):
  293. self.showCommands()
  294. else:
  295. _sys.stderr.write('Command not understood. Please try again.\n')
  296. self.showCommands()
  297. if state == 2:
  298. #game is over. write end time to the games table
  299. time = _datetime.utcnow().strftime("%Y-%m-%dT%H:%M")
  300. self.cur.execute('''UPDATE games SET endtime = "''' + time + '''" WHERE gameID = ''' + str(gameID))
  301. conn.commit()
  302. printStatus(tilestats=False)
  303. #### Is there a way to capture "ineffective" uses? For example,
  304. #### meeples that don't score points because they end up in a meadow that's
  305. #### controled by someone else?
  306. return 0
  307. def printStatus(self, tilestats=False):
  308. """
  309. Print the total score (current or final) for the specified gameID
  310. """
  311. _sys.stdout.write('\nCurrent Score\n')
  312. for player in self.players:
  313. a = self.cur.execute('SELECT points FROM scores WHERE gameID={0:d} and playerID={1:d}'.format(self.gameID, player[0]))
  314. res = a.fetchall()
  315. score = _np.sum(res)
  316. _sys.stdout.write('\t' + player[1]+ ': {0:1.0f}'.format(score) + '\n')
  317. _sys.stdout.write("{0:1.0f} tiles played, {1:1.0f} remaining.\n\n".format(self.ntile,
  318. self.totaltiles - self.ntile))
  319. def getCurrentPlayer(self):
  320. """
  321. Return the current player, determined by the turn number
  322. """
  323. return self.players[int((self.ntile - self.nbuilder) % len(self.players))]