fpdb/pyfpdb/HandHistoryConverter.py

351 lines
12 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
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)
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:
self.out_fh = open(self.out_path, 'a')
self.sitename = sitename
self.follow = follow
self.compiledPlayers = set()
self.maxseats = 10
2008-12-06 15:15:41 +01:00
def __str__(self):
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)
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)
if gametype is None:
return
if gametype[1] in ("hold", "omaha"):
hand = Hand.HoldemOmahaHand(self, self.sitename, gametype, handtext)
elif gametype[1] in ("razz","stud","stud8"):
hand = Hand.StudHand(self, self.sitename, gametype, handtext)
hand.writeHand(self.out_fh)
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')
2008-12-06 15:15:41 +01:00
self.gametype = self.determineGameType()
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
2009-02-25 11:32:12 +01:00
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)
2009-02-27 19:42:53 +01:00
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): abstract
# 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:
2009-02-27 19:42:53 +01:00
# 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