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.
21 _
= L10n
.get_translation()
38 pp
= pprint
.PrettyPrinter(indent
=4)
42 card_images
= 53 * [0]
46 """A Replayer to replay hands."""
47 def __init__(self
, config
, querylist
, mainwin
, options
= None, debug
=True):
50 self
.main_window
= mainwin
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)
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
]
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()
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()
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()
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
)
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)
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:
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())
158 if self
.cardImages
is None:
160 pb
= gtk
.gdk
.pixbuf_new_from_file(self
.deck_image
)
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:
180 state
= self
.states
[int(self
.state
.get_value())]
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
)
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
)
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
)
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
)
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)
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)
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():
281 image
.set_from_stock(gtk
.STOCK_MEDIA_PLAY
, gtk
.ICON_SIZE_BUTTON
)
282 self
.playPauseButton
.set_image(image
)
287 self
.state
.set_value(self
.state
.get_value() + 1)
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):
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
,))
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
)
325 def play_clicked(self
, button
):
326 self
.playing
= not self
.playing
329 image
.set_from_stock(gtk
.STOCK_MEDIA_PAUSE
, gtk
.ICON_SIZE_BUTTON
)
330 gobject
.timeout_add(1000, self
.increment_state
)
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
)
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
)
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
)
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>
358 def __init__(self
, stacks
, payouts
):
360 self
.payouts
= payouts
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
):
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:
375 eq
+= self
.getEquities((total
- stack
), player
, (depth
+ 1)) * (stack
/ D(total
))
376 self
.stacks
[i
] = stack
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)
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":
404 if phase
== "PREFLOP":
406 if phase
== "FLOP" and len(self
.flop
) == 0:
408 if phase
== "TURN" and len(self
.turn
) == 0:
410 if phase
== "RIVER" and len(self
.river
) == 0:
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)
425 elif phase
== "TURN":
427 elif phase
== "RIVER":
428 self
.showRiver
= 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":
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":
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]
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
476 def __init__(self
, hand
, name
, stack
, seat
):
477 self
.stack
= Decimal(stack
)
478 self
.chips
= Decimal(0)
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
)
488 """main can also be called in the python interpreter, by supplying the command line as the argument."""
492 def destroy(*args
): # call back for terminating the main eventloop
495 Configuration
.set_logfile("fpdb-log.txt")
498 (options
, argv
) = Options
.fpdb_options()
500 if options
.usage
== True:
501 #Print usage examples and exit
505 options
.sitename
= Options
.site_alias(options
.sitename
)
506 if options
.sitename
== False:
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)
523 if __name__
== '__main__':