fpdb/pyfpdb/HandHistoryConverter.py

360 lines
13 KiB
Python
Raw Normal View History

#!/usr/bin/python
#Copyright 2008 Carl Gherardi
#This program is free software: you can redistribute it and/or modify
#it under the terms of the GNU Affero General Public License as published by
#the Free Software Foundation, version 3 of the License.
#
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#GNU General Public License for more details.
#
#You should have received a copy of the GNU Affero General Public License
#along with this program. If not, see <http://www.gnu.org/licenses/>.
#In the "official" distribution you can find the license in
#agpl-3.0.txt in the docs folder of the package.
import Hand
import re
import sys
import threading
import traceback
2009-03-04 17:46:01 +01:00
import logging
from optparse import OptionParser
import os
import os.path
import xml.dom.minidom
import codecs
from decimal import Decimal
import operator
from xml.dom.minidom import Node
# from pokereval import PokerEval
import time
import datetime
import gettext
#from pokerengine.pokercards import *
# provides letter2name{}, letter2names{}, visible_card(), not_visible_card(), is_visible(), card_value(), class PokerCards
# but it's probably not installed so here are the ones we may want:
letter2name = {
'A': 'Ace',
'K': 'King',
'Q': 'Queen',
'J': 'Jack',
'T': 'Ten',
'9': 'Nine',
'8': 'Eight',
'7': 'Seven',
'6': 'Six',
'5': 'Five',
'4': 'Four',
'3': 'Trey',
'2': 'Deuce'
}
letter2names = {
'A': 'Aces',
'K': 'Kings',
'Q': 'Queens',
'J': 'Jacks',
'T': 'Tens',
'9': 'Nines',
'8': 'Eights',
'7': 'Sevens',
'6': 'Sixes',
'5': 'Fives',
'4': 'Fours',
'3': 'Treys',
'2': 'Deuces'
}
import gettext
gettext.install('myapplication')
class HandHistoryConverter(threading.Thread):
def __init__(self, in_path = '-', out_path = '-', sitename = None, follow=False):
threading.Thread.__init__(self)
2009-03-04 17:46:01 +01:00
logging.info("HandHistory init called")
# default filetype and codepage. Subclasses should set these properly.
2008-12-06 15:15:41 +01:00
self.filetype = "text"
self.codepage = "utf8"
self.in_path = in_path
self.out_path = out_path
if self.out_path == '-':
# write to stdout
self.out_fh = sys.stdout
else:
2009-03-06 19:10:04 +01:00
self.out_fh = open(self.out_path, 'a') #TODO: append may be overly conservative.
self.sitename = sitename
self.follow = follow
self.compiledPlayers = set()
self.maxseats = 10
2008-12-06 15:15:41 +01:00
def __str__(self):
#TODO : I got rid of most of the hhdir stuff.
2008-12-06 15:15:41 +01:00
tmp = "HandHistoryConverter: '%s'\n" % (self.sitename)
tmp = tmp + "\thhbase: '%s'\n" % (self.hhbase)
tmp = tmp + "\thhdir: '%s'\n" % (self.hhdir)
tmp = tmp + "\tfiletype: '%s'\n" % (self.filetype)
tmp = tmp + "\tinfile: '%s'\n" % (self.file)
2009-02-22 06:37:38 +01:00
tmp = tmp + "\toutfile: '%s'\n" % (self.ofile)
#tmp = tmp + "\tgametype: '%s'\n" % (self.gametype[0])
#tmp = tmp + "\tgamebase: '%s'\n" % (self.gametype[1])
#tmp = tmp + "\tlimit: '%s'\n" % (self.gametype[2])
#tmp = tmp + "\tsb/bb: '%s/%s'\n" % (self.gametype[3], self.gametype[4])
2008-12-06 15:15:41 +01:00
return tmp
def run(self):
if self.follow:
for handtext in self.tailHands():
self.processHand(handtext)
else:
handsList = self.allHands()
logging.info("Parsing %d hands" % len(handsList))
for handtext in handsList:
self.processHand(handtext)
2009-03-06 19:10:04 +01:00
if self.out_fh != sys.stdout:
self.ouf_fh.close()
def tailHands(self):
"""pseudo-code"""
while True:
ifile.tell()
text = ifile.read()
if nomoretext:
wait or sleep
else:
ahand = thenexthandinthetext
yield(ahand)
def allHands(self):
"""Return a list of handtexts in the file at self.in_path"""
self.readFile()
self.obs = self.obs.strip()
self.obs = self.obs.replace('\r\n', '\n')
if self.obs == "" or self.obs == None:
logging.info("Read no hands.")
return
return re.split(self.re_SplitHands, self.obs)
def processHand(self, handtext):
gametype = self.determineGameType(handtext)
2009-03-06 19:10:04 +01:00
logging.debug("gametype %s" % gametype)
if gametype is None:
return
hand = None
2009-03-06 19:10:04 +01:00
if gametype['game'] in ("hold", "omaha"):
hand = Hand.HoldemOmahaHand(self, self.sitename, gametype, handtext)
2009-03-06 19:10:04 +01:00
elif gametype['game'] in ("razz","stud","stud8"):
hand = Hand.StudHand(self, self.sitename, gametype, handtext)
if hand:
hand.writeHand(self.out_fh)
else:
2009-03-06 19:10:04 +01:00
logging.info("Unsupported game type: %s" % gametype)
# TODO: pity we don't know the HID at this stage. Log the entire hand?
# From the log we can deduce that it is the hand after the one before :)
2008-12-06 15:15:41 +01:00
def processFile(self):
starttime = time.time()
2008-12-06 15:15:41 +01:00
if not self.sanityCheck():
print "Cowardly refusing to continue after failed sanity check"
return
self.readFile(self.file)
if self.obs == "" or self.obs == None:
print "Did not read anything from file."
return
self.obs = self.obs.replace('\r\n', '\n')
self.gametype = self.determineGameType(self.obs)
if self.gametype == None:
print "Unknown game type from file, aborting on this file."
return
2008-12-06 15:15:41 +01:00
self.hands = self.splitFileIntoHands()
outfile = open(self.ofile, 'w')
2008-12-06 15:15:41 +01:00
for hand in self.hands:
#print "\nDEBUG: Input:\n"+hand.handText
2008-12-06 15:15:41 +01:00
self.readHandInfo(hand)
2008-12-06 15:15:41 +01:00
self.readPlayerStacks(hand)
2009-02-22 06:37:38 +01:00
#print "DEBUG stacks:", hand.stacks
# at this point we know the player names, they are in hand.players
playersThisHand = set([player[1] for player in hand.players])
if playersThisHand <= self.players: # x <= y means 'x is subset of y'
# we're ok; the regex should already cover them all.
pass
else:
# we need to recompile the player regexs.
self.players = playersThisHand
self.compilePlayerRegexs()
self.markStreets(hand)
# Different calls if stud or holdem like
2009-03-01 15:05:21 +01:00
if self.gametype[1] == "hold" or self.gametype[1] == "omahahi":
self.readBlinds(hand)
self.readButton(hand)
self.readHeroCards(hand) # want to generalise to draw games
elif self.gametype[1] == "razz" or self.gametype[1] == "stud" or self.gametype[1] == "stud8":
self.readAntes(hand)
self.readBringIn(hand)
self.readShowdownActions(hand)
# Read actions in street order
for street in hand.streetList: # go through them in order
# print "DEBUG: ", street
if hand.streets.group(street) is not None:
2009-03-01 15:05:21 +01:00
if self.gametype[1] == "hold" or self.gametype[1] == "omahahi":
self.readCommunityCards(hand, street) # read community cards
elif self.gametype[1] == "razz" or self.gametype[1] == "stud" or self.gametype[1] == "stud8":
self.readPlayerCards(hand, street)
self.readAction(hand, street)
2008-12-06 15:15:41 +01:00
2008-12-09 16:32:37 +01:00
self.readCollectPot(hand)
self.readShownCards(hand)
2008-12-09 16:32:37 +01:00
2008-12-06 15:15:41 +01:00
# finalise it (total the pot)
hand.totalPot()
self.getRake(hand)
hand.writeHand(outfile)
#if(hand.involved == True):
2008-12-06 15:15:41 +01:00
#self.writeHand("output file", hand)
#hand.printHand()
#else:
#pass #Don't write out observed hands
2008-12-06 15:15:41 +01:00
outfile.close()
endtime = time.time()
2009-02-22 06:37:38 +01:00
print "Processed %d hands in %.3f seconds" % (len(self.hands), endtime - starttime)
2008-12-06 15:15:41 +01:00
# These functions are parse actions that may be overridden by the inheriting class
# This function should return a list of lists looking like:
# return [["ring", "hold", "nl"], ["tour", "hold", "nl"]]
# Showing all supported games limits and types
2008-12-06 15:15:41 +01:00
def readSupportedGames(self): abstract
# should return a list
# type base limit
# [ ring, hold, nl , sb, bb ]
# Valid types specified in docs/tabledesign.html in Gametypes
def determineGameType(self, handText): abstract
2008-12-06 15:15:41 +01:00
# Read any of:
2009-02-27 19:42:53 +01:00
# HID HandID
# TABLE Table name
# SB small blind
# BB big blind
# GAMETYPE gametype
# YEAR MON DAY HR MIN SEC datetime
# BUTTON button seat number
2008-12-06 15:15:41 +01:00
def readHandInfo(self, hand): abstract
# Needs to return a list of lists in the format
# [['seat#', 'player1name', 'stacksize'] ['seat#', 'player2name', 'stacksize'] [...]]
def readPlayerStacks(self, hand): abstract
def compilePlayerRegexs(self): abstract
"""Compile dynamic regexes -- these explicitly match known player names and must be updated if a new player joins"""
2008-12-06 15:15:41 +01:00
# Needs to return a MatchObject with group names identifying the streets into the Hand object
# so groups are called by street names 'PREFLOP', 'FLOP', 'STREET2' etc
# blinds are done seperately
2008-12-06 15:15:41 +01:00
def markStreets(self, hand): abstract
#Needs to return a list in the format
# ['player1name', 'player2name', ...] where player1name is the sb and player2name is bb,
# addtional players are assumed to post a bb oop
def readBlinds(self, hand): abstract
def readAntes(self, hand): abstract
def readBringIn(self, hand): abstract
def readButton(self, hand): abstract
2008-12-06 15:15:41 +01:00
def readHeroCards(self, hand): abstract
2009-02-25 11:32:12 +01:00
def readPlayerCards(self, hand, street): abstract
2008-12-06 15:15:41 +01:00
def readAction(self, hand, street): abstract
2008-12-09 16:32:37 +01:00
def readCollectPot(self, hand): abstract
def readShownCards(self, hand): abstract
2008-12-06 15:15:41 +01:00
# Some sites don't report the rake. This will be called at the end of the hand after the pot total has been calculated
# an inheriting class can calculate it for the specific site if need be.
def getRake(self, hand):
hand.rake = hand.totalpot - hand.totalcollected # * Decimal('0.05') # probably not quite right
2008-12-06 15:15:41 +01:00
def sanityCheck(self):
sane = False
2008-12-06 15:15:41 +01:00
base_w = False
#Check if hhbase exists and is writable
#Note: Will not try to create the base HH directory
if not (os.access(self.hhbase, os.W_OK) and os.path.isdir(self.hhbase)):
print "HH Sanity Check: Directory hhbase '" + self.hhbase + "' doesn't exist or is not writable"
else:
#Check if hhdir exists and is writable
if not os.path.isdir(self.hhdir):
# In first pass, dir may not exist. Attempt to create dir
print "Creating directory: '%s'" % (self.hhdir)
os.mkdir(self.hhdir)
sane = True
elif os.access(self.hhdir, os.W_OK):
sane = True
else:
print "HH Sanity Check: Directory hhdir '" + self.hhdir + "' or its parent directory are not writable"
# Make sure input and output files are different or we'll overwrite the source file
if(self.ofile == self.file):
print "HH Sanity Check: output and input files are the same, check config"
2008-12-06 15:15:41 +01:00
return sane
# Functions not necessary to implement in sub class
def setFileType(self, filetype = "text", codepage='utf8'):
2008-12-06 15:15:41 +01:00
self.filetype = filetype
self.codepage = codepage
2008-12-06 15:15:41 +01:00
def splitFileIntoHands(self):
hands = []
self.obs = self.obs.strip()
list = self.re_SplitHands.split(self.obs)
2008-12-06 15:15:41 +01:00
list.pop() #Last entry is empty
for l in list:
# print "'" + l + "'"
hands = hands + [Hand.Hand(self.sitename, self.gametype, l)]
2008-12-06 15:15:41 +01:00
return hands
def readFile(self):
"""Read in path into self.obs or self.doc"""
2008-12-06 15:15:41 +01:00
if(self.filetype == "text"):
if self.in_path == '-':
# read from stdin
2009-02-26 16:36:23 +01:00
logging.debug("Reading stdin with %s" % self.codepage) # is this necessary? or possible? or what?
in_fh = codecs.getreader('cp1252')(sys.stdin)
else:
logging.debug("Opening %s with %s" % (self.in_path, self.codepage))
in_fh = codecs.open(self.in_path, 'r', self.codepage)
self.obs = in_fh.read()
in_fh.close()
2008-12-06 15:15:41 +01:00
elif(self.filetype == "xml"):
try:
doc = xml.dom.minidom.parse(filename)
self.doc = doc
except:
traceback.print_exc(file=sys.stderr)
def getStatus(self):
#TODO: Return a status of true if file processed ok
return True
def getProcessedFile(self):
return self.ofile