#!/usr/bin/env python # -*- coding: utf-8 -*- #Copyright 2009-2010 Grigorij Indigirkin #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 . #In the "official" distribution you can find the license in agpl-3.0.txt. """@package AlchemyMappings This package contains all classes to be mapped and mappers themselves """ #TODO: gettextify if file is used again import logging import re from decimal import Decimal from sqlalchemy.orm import mapper, relation, reconstructor from sqlalchemy.sql import select from collections import defaultdict from AlchemyTables import * from AlchemyFacilities import get_or_create, MappedBase from DerivedStats import DerivedStats from Exceptions import IncompleteHandError, FpdbError class Player(MappedBase): """Class reflecting Players db table""" @staticmethod def get_or_create(session, siteId, name): return get_or_create(Player, session, siteId=siteId, name=name)[0] def __str__(self): return '' % (self.name, self.site and self.site.name) class Gametype(MappedBase): """Class reflecting Gametypes db table""" @staticmethod def get_or_create(session, siteId, gametype): map = zip( ['type', 'base', 'category', 'limitType', 'smallBlind', 'bigBlind', 'smallBet', 'bigBet', 'currency'], ['type', 'base', 'category', 'limitType', 'sb', 'bb', 'dummy', 'dummy', 'currency']) gametype = dict([(new, gametype.get(old)) for new, old in map ]) hilo = "h" if gametype['category'] in ('studhilo', 'omahahilo'): hilo = "s" elif gametype['category'] in ('razz','27_3draw','badugi'): hilo = "l" gametype['hiLo'] = hilo for f in ['smallBlind', 'bigBlind', 'smallBet', 'bigBet']: if gametype[f] is None: gametype[f] = 0 gametype[f] = int(Decimal(gametype[f])*100) gametype['siteId'] = siteId return get_or_create(Gametype, session, **gametype)[0] class HandActions(object): """Class reflecting HandsActions db table""" def initFromImportedHand(self, hand, actions): self.hand = hand self.actions = {} for street, street_actions in actions.iteritems(): self.actions[street] = [] for v in street_actions: hp = hand.handplayers_by_name[v[0]] self.actions[street].append({'street': street, 'pid': hp.id, 'seat': hp.seatNo, 'action':v}) @property def flat_actions(self): actions = [] for street in self.hand.allStreets: actions += self.actions[street] return actions class HandInternal(DerivedStats): """Class reflecting Hands db table""" def parseImportedHandStep1(self, hand): """Extracts values to insert into from hand returned by HHC. No db is needed he""" hand.players = hand.getAlivePlayers() # also save some data for step2. Those fields aren't in Hands table self.siteId = hand.siteId self.gametype_dict = hand.gametype self.attachHandPlayers(hand) self.attachActions(hand) self.assembleHands(hand) self.assembleHandsPlayers(hand) def parseImportedHandStep2(self, session): """Fetching ids for gametypes and players""" gametype = Gametype.get_or_create(session, self.siteId, self.gametype_dict) self.gametypeId = gametype.id for hp in self.handPlayers: hp.playerId = Player.get_or_create(session, self.siteId, hp.name).id def getPlayerByName(self, name): if not hasattr(self, 'handplayers_by_name'): self.handplayers_by_name = {} for hp in self.handPlayers: pname = getattr(hp, 'name', None) or hp.player.name self.handplayers_by_name[pname] = hp return self.handplayers_by_name[name] def attachHandPlayers(self, hand): """Fill HandInternal.handPlayers list. Create self.handplayers_by_name""" hand.noSb = getattr(hand, 'noSb', None) if hand.noSb is None and self.gametype_dict['base']=='hold': saw_sb = False for action in hand.actions[hand.actionStreets[0]]: # blindsantes if action[1] == 'posts' and action[2] == 'small blind' and action[0] is not None: saw_sb = True hand.noSb = saw_sb self.handplayers_by_name = {} for seat, name, chips in hand.players: p = HandPlayer(hand = self, imported_hand=hand, seatNo=seat, name=name, startCash=chips) self.handplayers_by_name[name] = p def attachActions(self, hand): """Create HandActions object""" a = HandActions() a.initFromImportedHand(self, hand.actions) def parseImportedTournament(self, hand, session): """Fetching tourney, its type and players Must be called after Step2 """ if self.gametype_dict['type'] != 'tour': return # check for consistense for i in ('buyin', 'tourNo'): if not hasattr(hand, i): raise IncompleteHandError( "Field '%s' required for tournaments" % i, self.id, hand ) # repair old-style buyin value m = re.match('\$(\d+)\+\$(\d+)', hand.buyin) if m is not None: hand.buyin, self.fee = m.groups() # fetch tourney type tour_type_hand2db = { 'buyin': 'buyin', 'fee': 'fee', 'speed': 'speed', 'maxSeats': 'maxseats', 'knockout': 'isKO', 'rebuy': 'isRebuy', 'addOn': 'isAddOn', 'shootout': 'isShootout', 'matrix': 'isMatrix', 'sng': 'isSNG', } tour_type_index = dict([ ( i_db, getattr(hand, i_hand, None) ) for i_db, i_hand in tour_type_hand2db.iteritems() ]) tour_type_index['siteId'] = self.siteId tour_type = TourneyType.get_or_create(session, **tour_type_index) # fetch and update tourney tour = Tourney.get_or_create(session, hand.tourNo, tour_type.id) cols = tour.get_columns_names() for col in cols: hand_val = getattr(hand, col, None) if col in ('id', 'tourneyTypeId', 'comment', 'commentTs') or hand_val is None: continue db_val = getattr(tour, col, None) if db_val is None: setattr(tour, col, hand_val) elif col == 'koBounty': setattr(tour, col, max(db_val, hand_val)) elif col == 'tourStartTime' and hand.startTime: setattr(tour, col, min(db_val, hand.startTime)) if tour.entries is None and tour_type.sng: tour.entries = tour_type.maxSeats # fetch and update tourney players for hp in self.handPlayers: tp = TourneysPlayer.get_or_create(session, tour.id, hp.playerId) # FIXME: other TourneysPlayers should be added here session.flush() def isDuplicate(self, session): """Checks if current hand already exists in db siteHandNo ans gametypeId have to be setted """ return session.query(HandInternal).filter_by( siteHandNo=self.siteHandNo, gametypeId=self.gametypeId).count()!=0 def __str__(self): s = list() for i in self._sa_class_manager.mapper.c: s.append('%25s %s' % (i, getattr(self, i.name))) s+=['', ''] for i,p in enumerate(self.handPlayers): s.append('%d. %s' % (i, p.name or '???')) s.append(str(p)) return '\n'.join(s) @property def boardcards(self): cards = [] for i in range(5): cards.append(getattr(self, 'boardcard%d' % (i+1), None)) return filter(bool, cards) @property def HandClass(self): """Return HoldemOmahaHand or something like this""" import Hand if self.gametype.base == 'hold': return Hand.HoldemOmahaHand elif self.gametype.base == 'draw': return Hand.DrawHand elif self.gametype.base == 'stud': return Hand.StudHand raise Exception("Unknow gametype.base: '%s'" % self.gametype.base) @property def allStreets(self): return self.HandClass.allStreets @property def actionStreets(self): return self.HandClass.actionStreets class HandPlayer(MappedBase): """Class reflecting HandsPlayers db table""" def __init__(self, **kwargs): if 'imported_hand' in kwargs and 'seatNo' in kwargs: imported_hand = kwargs.pop('imported_hand') self.position = self.getPosition(imported_hand, kwargs['seatNo']) super(HandPlayer, self).__init__(**kwargs) @reconstructor def init_on_load(self): self.name = self.player.name @staticmethod def getPosition(hand, seat): """Returns position value like 'B', 'S', '0', '1', ... >>> class A(object): pass ... >>> A.noSb = False >>> A.maxseats = 6 >>> A.buttonpos = 2 >>> A.gametype = {'base': 'hold'} >>> A.players = [(i, None, None) for i in (2, 4, 5, 6)] >>> HandPlayer.getPosition(A, 6) # cut off '1' >>> HandPlayer.getPosition(A, 2) # button '0' >>> HandPlayer.getPosition(A, 4) # SB 'S' >>> HandPlayer.getPosition(A, 5) # BB 'B' >>> A.noSb = True >>> HandPlayer.getPosition(A, 5) # MP3 '2' >>> HandPlayer.getPosition(A, 6) # cut off '1' >>> HandPlayer.getPosition(A, 2) # button '0' >>> HandPlayer.getPosition(A, 4) # BB 'B' """ from itertools import chain if hand.gametype['base'] == 'stud': # FIXME: i've never played stud so plz check & del comment \\grindi bringin = None for action in chain(*[self.actions[street] for street in hand.allStreets]): if action[1]=='bringin': bringin = action[0] break if bringin is None: raise Exception, "Cannot find bringin" # name -> seat bringin = int(filter(lambda p: p[1]==bringin, bringin)[0]) seat = (int(seat) - int(bringin))%int(hand.maxseats) return str(seat) else: seats_occupied = sorted([seat_ for seat_, name, chips in hand.players], key=int) if hand.buttonpos not in seats_occupied: # i.e. something like # Seat 3: PlayerX ($0), is sitting out # The button is in seat #3 hand.buttonpos = max(seats_occupied, key = lambda s: int(s) if int(s) <= int(hand.buttonpos) else int(s) - int(hand.maxseats) ) seats_occupied = sorted(seats_occupied, key = lambda seat_: ( - seats_occupied.index(seat_) + seats_occupied.index(hand.buttonpos) + 2) % len(seats_occupied) ) # now (if SB presents) seats_occupied contains seats in order: BB, SB, BU, CO, MP3, ... if hand.noSb: # fix order in the case nosb seats_occupied = seats_occupied[1:] + seats_occupied[0:1] seats_occupied.insert(1, -1) seat = seats_occupied.index(seat) if seat == 0: return 'B' elif seat == 1: return 'S' else: return str(seat-2) @property def cards(self): cards = [] for i in range(7): cards.append(getattr(self, 'card%d' % (i+1), None)) return filter(bool, cards) def __str__(self): s = list() for i in self._sa_class_manager.mapper.c: s.append('%45s %s' % (i, getattr(self, i.name))) return '\n'.join(s) class Site(object): """Class reflecting Players db table""" INITIAL_DATA = [ (1 , 'Full Tilt Poker','FT'), (2 , 'PokerStars', 'PS'), (3 , 'Everleaf', 'EV'), (4 , 'Win2day', 'W2'), (5 , 'OnGame', 'OG'), (6 , 'UltimateBet', 'UB'), (7 , 'Betfair', 'BF'), (8 , 'Absolute', 'AB'), (9 , 'PartyPoker', 'PP'), (10, 'Partouche', 'PA'), (11, 'Carbon', 'CA'), (12, 'PKR', 'PK'), ] INITIAL_DATA_KEYS = ('id', 'name', 'code') INITIAL_DATA_DICTS = [ dict(zip(INITIAL_DATA_KEYS, datum)) for datum in INITIAL_DATA ] @classmethod def insert_initial(cls, connection): connection.execute(sites_table.insert(), cls.INITIAL_DATA_DICTS) class Tourney(MappedBase): """Class reflecting Tourneys db table""" @classmethod def get_or_create(cls, session, siteTourneyNo, tourneyTypeId): """Fetch tourney by index or creates one if none. """ return get_or_create(cls, session, siteTourneyNo=siteTourneyNo, tourneyTypeId=tourneyTypeId)[0] class TourneyType(MappedBase): """Class reflecting TourneyType db table""" @classmethod def get_or_create(cls, session, **kwargs): """Fetch tourney type by index or creates one if none Required kwargs: buyin fee speed maxSeats knockout rebuy addOn shootout matrix sng currency """ return get_or_create(cls, session, **kwargs)[0] class TourneysPlayer(MappedBase): """Class reflecting TourneysPlayers db table""" @classmethod def get_or_create(cls, session, tourneyId, playerId): """Fetch tourney player by index or creates one if none """ return get_or_create(cls, session, tourneyId=tourneyId, playerId=playerId) class Version(object): """Provides read/write access for version var""" CURRENT_VERSION = 120 # db version for current release # 119 - first alchemy version # 120 - add m_factor conn = None ver = None def __init__(self, connection=None): if self.__class__.conn is None: self.__class__.conn = connection @classmethod def is_wrong(cls): return cls.get() != cls.CURRENT_VERSION @classmethod def get(cls): if cls.ver is None: try: cls.ver = cls.conn.execute(select(['version'], settings_table)).fetchone()[0] except: return None return cls.ver @classmethod def set(cls, value): if cls.conn.execute(settings_table.select()).rowcount==0: cls.conn.execute(settings_table.insert(), version=value) else: cls.conn.execute(settings_table.update().values(version=value)) cls.ver = value @classmethod def set_initial(cls): cls.set(cls.CURRENT_VERSION) mapper (Gametype, gametypes_table, properties={ 'hands': relation(HandInternal, backref='gametype'), }) mapper (Player, players_table, properties={ 'playerHands': relation(HandPlayer, backref='player'), 'playerTourney': relation(TourneysPlayer, backref='player'), }) mapper (Site, sites_table, properties={ 'gametypes': relation(Gametype, backref = 'site'), 'players': relation(Player, backref = 'site'), 'tourneyTypes': relation(TourneyType, backref = 'site'), }) mapper (HandActions, hands_actions_table, properties={}) mapper (HandInternal, hands_table, properties={ 'handPlayers': relation(HandPlayer, backref='hand'), 'actions_all': relation(HandActions, backref='hand', uselist=False), }) mapper (HandPlayer, hands_players_table, properties={}) mapper (Tourney, tourneys_table) mapper (TourneyType, tourney_types_table, properties={ 'tourneys': relation(Tourney, backref='type'), }) mapper (TourneysPlayer, tourneys_players_table) class LambdaKeyDict(defaultdict): """Operates like defaultdict but passes key argument to the factory function""" def __missing__(key): return self.default_factory(key)