Use debian 2.7 only
[fpbd-bostik.git] / pyfpdb / GuiReplayer.py
blobb1fcba36960670ad84ffff77a0a2fa190fc9691f
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
4 #Copyright 2010-2011 Maxime Grandchamp
5 #This program is free software: you can redistribute it and/or modify
6 #it under the terms of the GNU Affero General Public License as published by
7 #the Free Software Foundation, version 3 of the License.
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.
14 #You should have received a copy of the GNU Affero General Public License
15 #along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #In the "official" distribution you can find the license in agpl-3.0.txt.
18 # Note that this now contains the replayer only! The list of hands has been moved to GuiHandViewer by zarturo.
20 import L10n
21 _ = L10n.get_translation()
24 from Hand import *
25 import Configuration
26 import Database
27 import SQL
28 import fpdb_import
29 import pygtk
30 pygtk.require('2.0')
31 import gtk
32 import math
33 import gobject
35 import copy
37 import pprint
38 pp = pprint.PrettyPrinter(indent=4)
41 global card_images
42 card_images = 53 * [0]
45 class GuiReplayer:
46 """A Replayer to replay hands."""
47 def __init__(self, config, querylist, mainwin, options = None, debug=True):
48 self.debug = debug
49 self.conf = config
50 self.main_window = mainwin
51 self.sql = querylist
53 self.db = Database.Database(self.conf, sql=self.sql)
54 self.states = [] # List with all table states.
56 self.window = gtk.Window()
57 self.window.set_title("FPDB Hand Replayer")
59 self.replayBox = gtk.VBox(False, 0)
60 self.replayBox.show()
62 self.window.add(self.replayBox)
65 self.area=gtk.DrawingArea()
66 self.pangolayout = self.area.create_pango_layout("")
67 self.area.connect("expose-event", self.area_expose)
68 self.style = self.area.get_style()
69 self.gc = self.style.fg_gc[gtk.STATE_NORMAL]
70 self.area.show()
72 self.replayBox.pack_start(self.area, False)
74 self.buttonBox = gtk.HButtonBox()
75 self.buttonBox.set_layout(gtk.BUTTONBOX_SPREAD)
76 self.startButton = gtk.Button()
77 image = gtk.Image()
78 image.set_from_stock(gtk.STOCK_MEDIA_PREVIOUS, gtk.ICON_SIZE_BUTTON)
79 self.startButton.set_image(image)
80 self.startButton.connect("clicked", self.start_clicked)
81 self.flopButton = gtk.Button("Flop")
82 self.flopButton.connect("clicked", self.flop_clicked)
83 self.turnButton = gtk.Button("Turn")
84 self.turnButton.connect("clicked", self.turn_clicked)
85 self.riverButton = gtk.Button("River")
86 self.riverButton.connect("clicked", self.river_clicked)
87 self.endButton = gtk.Button()
88 image = gtk.Image()
89 image.set_from_stock(gtk.STOCK_MEDIA_NEXT, gtk.ICON_SIZE_BUTTON)
90 self.endButton.set_image(image)
91 self.endButton.connect("clicked", self.end_clicked)
92 self.playPauseButton = gtk.Button()
93 image = gtk.Image()
94 image.set_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_BUTTON)
95 self.playPauseButton.set_image(image)
96 self.playPauseButton.connect("clicked", self.play_clicked)
97 self.buttonBox.add(self.startButton)
98 self.buttonBox.add(self.flopButton)
99 self.buttonBox.add(self.turnButton)
100 self.buttonBox.add(self.riverButton)
101 self.buttonBox.add(self.endButton)
102 self.buttonBox.add(self.playPauseButton)
103 self.buttonBox.show_all()
105 self.replayBox.pack_start(self.buttonBox, False)
107 self.state = gtk.Adjustment(0, 0, 0, 1)
108 self.stateSlider = gtk.HScale(self.state)
109 self.stateSlider.connect("format_value", lambda x,y: "")
110 self.stateSlider.set_digits(0)
111 self.handler_id = self.state.connect("value_changed", self.slider_changed)
112 self.stateSlider.show()
114 self.replayBox.pack_start(self.stateSlider, False)
116 self.window.show_all()
118 self.window.connect('destroy', self.on_destroy)
120 self.playing = False
122 self.deck_image = "Cards01.png" #FIXME: read from config (requires deck to be defined somewhere appropriate
123 self.tableImage = None
124 self.playerBackdrop = None
125 self.cardImages = None
126 #NOTE: There are two caches of card images as I haven't found a way to
127 # replicate the copy_area() function from Pixbuf in the Pixmap class
128 # cardImages is used for the tables display card_images is used for the
129 # table display. Sooner or later we should probably use one or the other.
130 card_images = self.init_card_images(config)
132 def init_card_images(self, config):
133 suits = ('s', 'h', 'd', 'c')
134 ranks = (14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2)
135 pb = gtk.gdk.pixbuf_new_from_file(config.execution_path(self.deck_image))
137 for j in range(0, 13):
138 for i in range(0, 4):
139 loc = Card.cardFromValueSuit(ranks[j], suits[i])
140 card_images[loc] = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, pb.get_has_alpha(), pb.get_bits_per_sample(), 30, 42)
141 pb.copy_area(30*j, 42*i, 30, 42, card_images[loc], 0, 0)
142 card_images[0] = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, pb.get_has_alpha(), pb.get_bits_per_sample(), 30, 42)
143 pb.copy_area(30*13, 0, 30, 42, card_images[0], 0, 0)
144 return card_images
147 def area_expose(self, area, event):
148 self.style = self.area.get_style()
149 self.gc = self.style.fg_gc[gtk.STATE_NORMAL]
151 if self.tableImage is None or self.playerBackdrop is None:
152 try:
153 self.playerBackdrop = gtk.gdk.pixbuf_new_from_file("../gfx/playerbackdrop.png")
154 self.tableImage = gtk.gdk.pixbuf_new_from_file("../gfx/Table.png")
155 self.area.set_size_request(self.tableImage.get_width(), self.tableImage.get_height())
156 except:
157 return
158 if self.cardImages is None:
159 try:
160 pb = gtk.gdk.pixbuf_new_from_file(self.deck_image)
161 except:
162 return
163 self.cardwidth = pb.get_width() / 14
164 self.cardheight = pb.get_height() / 6
166 self.cardImages = [gtk.gdk.Pixmap(self.area.window, self.cardwidth, self.cardheight) for i in range(53)]
167 suits = ('s', 'h', 'd', 'c')
168 ranks = (14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2)
169 for j in range(0, 13):
170 for i in range(0, 4):
171 index = Card.cardFromValueSuit(ranks[j], suits[i])
172 self.cardImages[index].draw_pixbuf(self.gc, pb, self.cardwidth * j, self.cardheight * i, 0, 0, self.cardwidth, self.cardheight)
173 self.cardImages[0].draw_pixbuf(self.gc, pb, self.cardwidth*13, self.cardheight*2, 0, 0, self.cardwidth, self.cardheight)
175 self.area.window.draw_pixbuf(self.gc, self.tableImage, 0, 0, 0, 0)
177 if len(self.states) == 0:
178 return
180 state = self.states[int(self.state.get_value())]
182 padding = 6
183 communityLeft = int(self.tableImage.get_width() / 2 - 2.5 * self.cardwidth - 2 * padding)
184 communityTop = int(self.tableImage.get_height() / 2 - 1.5 * self.cardheight)
186 cm = self.gc.get_colormap() #create colormap toi be able to play with colours
188 color = cm.alloc_color("white") #defaults to black
189 self.gc.set_foreground(color)
191 convertx = lambda x: int(x * self.tableImage.get_width() * 0.8) + self.tableImage.get_width() / 2
192 converty = lambda y: int(y * self.tableImage.get_height() * 0.6) + self.tableImage.get_height() / 2
194 for player in state.players.values():
195 playerx = convertx(player.x)
196 playery = converty(player.y)
197 self.area.window.draw_pixbuf(self.gc, self.playerBackdrop, 0, 0, playerx - self.playerBackdrop.get_width() / 2, playery - padding / 2)
198 if player.action=="folds":
199 color = cm.alloc_color("grey") #player has folded => greyed out
200 self.gc.set_foreground(color)
201 else:
202 color = cm.alloc_color("white") #player is live
203 self.gc.set_foreground(color)
204 if state.gametype == 'holdem':
205 cardIndex = Card.encodeCard(player.holecards[0:2])
206 self.area.window.draw_drawable(self.gc, self.cardImages[cardIndex], 0, 0, playerx - self.cardwidth - padding / 2, playery - self.cardheight, -1, -1)
207 cardIndex = Card.encodeCard(player.holecards[3:5])
208 self.area.window.draw_drawable(self.gc, self.cardImages[cardIndex], 0, 0, playerx + padding / 2, playery - self.cardheight, -1, -1)
209 elif state.gametype in ('omahahi', 'omahahilo'):
210 cardIndex = Card.encodeCard(player.holecards[0:2])
211 self.area.window.draw_drawable(self.gc, self.cardImages[cardIndex], 0, 0, playerx - 2 * self.cardwidth - 3 * padding / 2, playery - self.cardheight, -1, -1)
212 cardIndex = Card.encodeCard(player.holecards[3:5])
213 self.area.window.draw_drawable(self.gc, self.cardImages[cardIndex], 0, 0, playerx - self.cardwidth - padding / 2, playery - self.cardheight, -1, -1)
214 cardIndex = Card.encodeCard(player.holecards[6:8])
215 self.area.window.draw_drawable(self.gc, self.cardImages[cardIndex], 0, 0, playerx + padding / 2, playery - self.cardheight, -1, -1)
216 cardIndex = Card.encodeCard(player.holecards[9:11])
217 self.area.window.draw_drawable(self.gc, self.cardImages[cardIndex], 0, 0, playerx + self.cardwidth + 3 * padding / 2, playery - self.cardheight, -1, -1)
219 color_string = '#FFFFFF'
220 background_color = ''
221 self.pangolayout.set_markup('<span foreground="%s" size="medium">%s %s%.2f</span>' % (color_string, player.name, self.currency, player.stack))
222 self.area.window.draw_layout(self.gc, playerx - self.pangolayout.get_pixel_size()[0] / 2, playery, self.pangolayout)
224 if player.justacted:
225 color_string = '#FF0000'
226 background_color = 'background="#000000" '
227 self.pangolayout.set_markup('<span foreground="%s" size="medium">%s</span>' % (color_string, player.action))
228 self.area.window.draw_layout(self.gc, playerx - self.pangolayout.get_pixel_size()[0] / 2, playery + self.pangolayout.get_pixel_size()[1], self.pangolayout)
229 else:
230 color_string = '#FFFF00'
231 background_color = ''
232 if player.chips != 0: #displays amount
233 self.pangolayout.set_markup('<span foreground="%s" %s weight="heavy" size="large">%s%.2f</span>' % (color_string, background_color, self.currency, player.chips))
234 self.area.window.draw_layout(self.gc, convertx(player.x * .65) - self.pangolayout.get_pixel_size()[0] / 2, converty(player.y * 0.65), self.pangolayout)
236 color_string = '#FFFFFF'
238 self.pangolayout.set_markup('<span foreground="%s" size="large">%s%.2f</span>' % (color_string, self.currency, state.pot)) #displays pot
239 self.area.window.draw_layout(self.gc,self.tableImage.get_width() / 2 - self.pangolayout.get_pixel_size()[0] / 2, self.tableImage.get_height() / 2, self.pangolayout)
241 if state.showFlop:
242 cardIndex = Card.encodeCard(state.flop[0])
243 self.area.window.draw_drawable(self.gc, self.cardImages[cardIndex], 0, 0, communityLeft, communityTop, -1, -1)
244 cardIndex = Card.encodeCard(state.flop[1])
245 self.area.window.draw_drawable(self.gc, self.cardImages[cardIndex], 0, 0, communityLeft + self.cardwidth + padding, communityTop, -1, -1)
246 cardIndex = Card.encodeCard(state.flop[2])
247 self.area.window.draw_drawable(self.gc, self.cardImages[cardIndex], 0, 0, communityLeft + 2 * (self.cardwidth + padding), communityTop, -1, -1)
248 if state.showTurn:
249 cardIndex = Card.encodeCard(state.turn[0])
250 self.area.window.draw_drawable(self.gc, self.cardImages[cardIndex], 0, 0, communityLeft + 3 * (self.cardwidth + padding), communityTop, -1, -1)
251 if state.showRiver:
252 cardIndex = Card.encodeCard(state.river[0])
253 self.area.window.draw_drawable(self.gc, self.cardImages[cardIndex], 0, 0, communityLeft + 4 * (self.cardwidth + padding), communityTop, -1, -1)
255 color = cm.alloc_color("black") #we don't want to draw the filters and others in red
256 self.gc.set_foreground(color)
258 def play_hand(self, hand):
259 actions = hand.allStreets
260 state = TableState(hand)
261 for action in actions:
262 state = copy.deepcopy(state)
263 if state.startPhase(action):
264 self.states.append(state)
265 for i in range(0,len(hand.actions[action])):
266 state = copy.deepcopy(state)
267 state.updateForAction(hand.actions[action][i])
268 self.states.append(state)
269 state = copy.deepcopy(state)
270 state.endHand(hand.collectees)
271 self.states.append(state)
273 self.state.set_value(0)
274 self.state.set_upper(len(self.states) - 1)
275 self.state.value_changed()
277 def increment_state(self):
278 if self.state.get_value() == self.state.get_upper():
279 self.playing = False
280 image = gtk.Image()
281 image.set_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_BUTTON)
282 self.playPauseButton.set_image(image)
284 if not self.playing:
285 return False
287 self.state.set_value(self.state.get_value() + 1)
288 return True
290 def on_destroy(self, window):
291 """ Prevent replayer from continue playing after window is closed """
292 self.state.disconnect(self.handler_id)
294 def slider_changed(self, adjustment):
295 alloc = self.area.get_allocation()
296 rect = gtk.gdk.Rectangle(0, 0, alloc.width, alloc.height)
297 self.area.window.invalidate_rect(rect, True) #make sure we refresh the whole screen
298 self.area.window.process_updates(True)
300 def importhand(self, handid=1):
301 # Fetch hand info
302 # We need at least sitename, gametype, handid
303 # for the Hand.__init__
305 ####### Shift this section in Database.py for all to use ######
306 q = self.sql.query['get_gameinfo_from_hid']
307 q = q.replace('%s', self.sql.query['placeholder'])
309 c = self.db.get_cursor()
311 c.execute(q, (handid,))
312 res = c.fetchone()
313 gametype = {'category':res[1],'base':res[2],'type':res[3],'limitType':res[4],'hilo':res[5],'sb':res[6],'bb':res[7], 'currency':res[10]}
314 #FIXME: smallbet and bigbet are res[8] and res[9] respectively
315 ###### End section ########
316 if gametype['base'] == 'hold':
317 h = HoldemOmahaHand(config = self.conf, hhc = None, sitename=res[0], gametype = gametype, handText=None, builtFrom = "DB", handid=handid)
318 elif gametype['base'] == 'stud':
319 h = StudHand(config = self.conf, hhc = None, sitename=res[0], gametype = gametype, handText=None, builtFrom = "DB", handid=handid)
320 elif gametype['base'] == 'draw':
321 h = DrawHand(config = self.conf, hhc = None, sitename=res[0], gametype = gametype, handText=None, builtFrom = "DB", handid=handid)
322 h.select(self.db, handid)
323 return h
325 def play_clicked(self, button):
326 self.playing = not self.playing
327 image = gtk.Image()
328 if self.playing:
329 image.set_from_stock(gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_BUTTON)
330 gobject.timeout_add(1000, self.increment_state)
331 else:
332 image.set_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_BUTTON)
333 self.playPauseButton.set_image(image)
334 def start_clicked(self, button):
335 self.state.set_value(0)
336 def end_clicked(self, button):
337 self.state.set_value(self.state.get_upper())
338 def flop_clicked(self, button):
339 for i in range(0, len(self.states)):
340 if self.states[i].showFlop:
341 self.state.set_value(i)
342 break
343 def turn_clicked(self, button):
344 for i in range(0, len(self.states)):
345 if self.states[i].showTurn:
346 self.state.set_value(i)
347 break
348 def river_clicked(self, button):
349 for i in range(0, len(self.states)):
350 if self.states[i].showRiver:
351 self.state.set_value(i)
352 break
354 # ICM code originally grabbed from http://svn.gna.org/svn/pokersource/trunk/icm-calculator/icm-webservice.py
355 # Copyright (c) 2008 Thomas Johnson <tomfmason@gmail.com>
357 class ICM:
358 def __init__(self, stacks, payouts):
359 self.stacks = stacks
360 self.payouts = payouts
361 self.equities = []
362 self.prepare()
363 def prepare(self):
364 total = sum(self.stacks)
365 for k,v in enumerate(stacks):
366 self.equities.append(round(Decimal(str(self.getEquities(total, k,0))),4))
367 def getEquities(self, total, player, depth):
368 D = Decimal
369 eq = D(self.stacks[player]) / total * D(str(self.payouts[depth]))
370 if(depth + 1 < len(self.payouts)):
372 for stack in self.stacks:
373 if i != player and stack > 0.0:
374 self.stacks[i] = 0.0
375 eq += self.getEquities((total - stack), player, (depth + 1)) * (stack / D(total))
376 self.stacks[i] = stack
377 i += 1
378 return eq
380 class TableState:
381 def __init__(self, hand):
382 self.pot = Decimal(0)
383 self.flop = hand.board["FLOP"]
384 self.turn = hand.board["TURN"]
385 self.river = hand.board["RIVER"]
386 self.showFlop = False
387 self.showTurn = False
388 self.showRiver = False
389 self.bet = Decimal(0)
390 self.called = Decimal(0)
391 self.gametype = hand.gametype['category']
392 # NOTE: Need a useful way to grab payouts
393 #self.icm = ICM(stacks,payouts)
394 #print icm.equities
396 self.players = {}
398 for seat, name, chips, pos in hand.players:
399 self.players[name] = Player(hand, name, chips, seat)
401 def startPhase(self, phase):
402 if phase == "BLINDSANTES":
403 return True
404 if phase == "PREFLOP":
405 return False
406 if phase == "FLOP" and len(self.flop) == 0:
407 return False
408 if phase == "TURN" and len(self.turn) == 0:
409 return False
410 if phase == "RIVER" and len(self.river) == 0:
411 return False
413 for player in self.players.values():
414 player.justacted = False
415 if player.chips > self.called:
416 player.stack += player.chips - self.called
417 player.chips = self.called
418 self.pot += player.chips
419 player.chips = Decimal(0)
420 self.bet = Decimal(0)
421 self.called = Decimal(0)
423 if phase == "FLOP":
424 self.showFlop = True
425 elif phase == "TURN":
426 self.showTurn = True
427 elif phase == "RIVER":
428 self.showRiver = True
430 return True
432 def updateForAction(self, action):
433 for player in self.players.values():
434 player.justacted = False
436 player = self.players[action[0]]
437 player.action = action[1]
438 player.justacted = True
439 if action[1] == "folds" or action[1] == "checks":
440 pass
441 elif action[1] == "raises" or action[1] == "bets":
442 self.called = Decimal(0)
443 diff = self.bet - player.chips
444 self.bet += action[2]
445 player.chips += action[2] + diff
446 player.stack -= action[2] + diff
447 elif action[1] == "big blind":
448 self.bet = action[2]
449 player.chips += action[2]
450 player.stack -= action[2]
451 elif action[1] == "calls" or action[1] == "small blind" or action[1] == "secondsb":
452 player.chips += action[2]
453 player.stack -= action[2]
454 self.called = max(self.called, player.chips)
455 elif action[1] == "both":
456 player.chips += action[2]
457 player.stack -= action[2]
458 elif action[1] == "ante":
459 self.pot += action[2]
460 player.stack -= action[2]
461 else:
462 print "unhandled action: " + str(action)
464 def endHand(self, collectees):
465 self.pot = Decimal(0)
466 for player in self.players.values():
467 player.justacted = False
468 player.chips = Decimal(0)
469 for name,amount in collectees.items():
470 player = self.players[name]
471 player.chips += amount
472 player.action = "collected"
473 player.justacted = True
475 class Player:
476 def __init__(self, hand, name, stack, seat):
477 self.stack = Decimal(stack)
478 self.chips = Decimal(0)
479 self.seat = seat
480 self.name = name
481 self.action = None
482 self.justacted = False
483 self.holecards = hand.join_holecards(name)
484 self.x = 0.5 * math.cos(2 * self.seat * math.pi / hand.maxseats)
485 self.y = 0.5 * math.sin(2 * self.seat * math.pi / hand.maxseats)
487 def main(argv=None):
488 """main can also be called in the python interpreter, by supplying the command line as the argument."""
489 if argv is None:
490 argv = sys.argv[1:]
492 def destroy(*args): # call back for terminating the main eventloop
493 gtk.main_quit()
495 Configuration.set_logfile("fpdb-log.txt")
496 import Options
498 (options, argv) = Options.fpdb_options()
500 if options.usage == True:
501 #Print usage examples and exit
502 sys.exit(0)
504 if options.sitename:
505 options.sitename = Options.site_alias(options.sitename)
506 if options.sitename == False:
507 usage()
509 config = Configuration.Config(file = "HUD_config.test.xml")
510 db = Database.Database(config)
511 sql = SQL.Sql(db_server = 'sqlite')
513 main_window = gtk.Window()
514 main_window.connect('destroy', destroy)
516 replayer = GuiReplayer(config, sql, main_window, options=options, debug=True)
518 main_window.add(replayer.get_vbox())
519 main_window.set_default_size(800,800)
520 main_window.show()
521 gtk.main()
523 if __name__ == '__main__':
524 sys.exit(main())