If you only have games of a single limit type (fixed, pot, or no limit), but of more...
[fpdb-dooglus.git] / pyfpdb / AlchemyMappings.py
blobb5891c25753319e401a13dabf27a7f56c31414de
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
4 #Copyright 2009-2010 Grigorij Indigirkin
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 """@package AlchemyMappings
19 This package contains all classes to be mapped and mappers themselves
20 """
22 #TODO: gettextify if file is used again
24 import logging
25 import re
26 from decimal import Decimal
27 from sqlalchemy.orm import mapper, relation, reconstructor
28 from sqlalchemy.sql import select
29 from collections import defaultdict
32 from AlchemyTables import *
33 from AlchemyFacilities import get_or_create, MappedBase
34 from DerivedStats import DerivedStats
35 from Exceptions import IncompleteHandError, FpdbError
38 class Player(MappedBase):
39 """Class reflecting Players db table"""
41 @staticmethod
42 def get_or_create(session, siteId, name):
43 return get_or_create(Player, session, siteId=siteId, name=name)[0]
45 def __str__(self):
46 return '<Player "%s" on %s>' % (self.name, self.site and self.site.name)
49 class Gametype(MappedBase):
50 """Class reflecting Gametypes db table"""
52 @staticmethod
53 def get_or_create(session, siteId, gametype):
54 map = zip(
55 ['type', 'base', 'category', 'limitType', 'smallBlind', 'bigBlind', 'smallBet', 'bigBet', 'currency'],
56 ['type', 'base', 'category', 'limitType', 'sb', 'bb', 'dummy', 'dummy', 'currency'])
57 gametype = dict([(new, gametype.get(old)) for new, old in map ])
59 hilo = "h"
60 if gametype['category'] in ('studhilo', 'omahahilo'):
61 hilo = "s"
62 elif gametype['category'] in ('razz','27_3draw','badugi'):
63 hilo = "l"
64 gametype['hiLo'] = hilo
66 for f in ['smallBlind', 'bigBlind', 'smallBet', 'bigBet']:
67 if gametype[f] is None:
68 gametype[f] = 0
69 gametype[f] = int(Decimal(gametype[f])*100)
71 gametype['siteId'] = siteId
72 return get_or_create(Gametype, session, **gametype)[0]
75 class HandActions(object):
76 """Class reflecting HandsActions db table"""
77 def initFromImportedHand(self, hand, actions):
78 self.hand = hand
79 self.actions = {}
80 for street, street_actions in actions.iteritems():
81 self.actions[street] = []
82 for v in street_actions:
83 hp = hand.handplayers_by_name[v[0]]
84 self.actions[street].append({'street': street, 'pid': hp.id, 'seat': hp.seatNo, 'action':v})
86 @property
87 def flat_actions(self):
88 actions = []
89 for street in self.hand.allStreets:
90 actions += self.actions[street]
91 return actions
95 class HandInternal(DerivedStats):
96 """Class reflecting Hands db table"""
98 def parseImportedHandStep1(self, hand):
99 """Extracts values to insert into from hand returned by HHC. No db is needed he"""
100 hand.players = hand.getAlivePlayers()
102 # also save some data for step2. Those fields aren't in Hands table
103 self.siteId = hand.siteId
104 self.gametype_dict = hand.gametype
106 self.attachHandPlayers(hand)
107 self.attachActions(hand)
109 self.assembleHands(hand)
110 self.assembleHandsPlayers(hand)
112 def parseImportedHandStep2(self, session):
113 """Fetching ids for gametypes and players"""
114 gametype = Gametype.get_or_create(session, self.siteId, self.gametype_dict)
115 self.gametypeId = gametype.id
116 for hp in self.handPlayers:
117 hp.playerId = Player.get_or_create(session, self.siteId, hp.name).id
119 def getPlayerByName(self, name):
120 if not hasattr(self, 'handplayers_by_name'):
121 self.handplayers_by_name = {}
122 for hp in self.handPlayers:
123 pname = getattr(hp, 'name', None) or hp.player.name
124 self.handplayers_by_name[pname] = hp
125 return self.handplayers_by_name[name]
127 def attachHandPlayers(self, hand):
128 """Fill HandInternal.handPlayers list. Create self.handplayers_by_name"""
129 hand.noSb = getattr(hand, 'noSb', None)
130 if hand.noSb is None and self.gametype_dict['base']=='hold':
131 saw_sb = False
132 for action in hand.actions[hand.actionStreets[0]]: # blindsantes
133 if action[1] == 'posts' and action[2] == 'small blind' and action[0] is not None:
134 saw_sb = True
135 hand.noSb = saw_sb
137 self.handplayers_by_name = {}
138 for seat, name, chips in hand.players:
139 p = HandPlayer(hand = self, imported_hand=hand, seatNo=seat,
140 name=name, startCash=chips)
141 self.handplayers_by_name[name] = p
143 def attachActions(self, hand):
144 """Create HandActions object"""
145 a = HandActions()
146 a.initFromImportedHand(self, hand.actions)
148 def parseImportedTournament(self, hand, session):
149 """Fetching tourney, its type and players
151 Must be called after Step2
153 if self.gametype_dict['type'] != 'tour': return
155 # check for consistense
156 for i in ('buyin', 'tourNo'):
157 if not hasattr(hand, i):
158 raise IncompleteHandError(
159 "Field '%s' required for tournaments" % i, self.id, hand )
161 # repair old-style buyin value
162 m = re.match('\$(\d+)\+\$(\d+)', hand.buyin)
163 if m is not None:
164 hand.buyin, self.fee = m.groups()
166 # fetch tourney type
167 tour_type_hand2db = {
168 'buyin': 'buyin',
169 'fee': 'fee',
170 'speed': 'speed',
171 'maxSeats': 'maxseats',
172 'knockout': 'isKO',
173 'rebuy': 'isRebuy',
174 'addOn': 'isAddOn',
175 'shootout': 'isShootout',
176 'matrix': 'isMatrix',
177 'sng': 'isSNG',
179 tour_type_index = dict([
180 ( i_db, getattr(hand, i_hand, None) )
181 for i_db, i_hand in tour_type_hand2db.iteritems()
183 tour_type_index['siteId'] = self.siteId
184 tour_type = TourneyType.get_or_create(session, **tour_type_index)
186 # fetch and update tourney
187 tour = Tourney.get_or_create(session, hand.tourNo, tour_type.id)
188 cols = tour.get_columns_names()
189 for col in cols:
190 hand_val = getattr(hand, col, None)
191 if col in ('id', 'tourneyTypeId', 'comment', 'commentTs') or hand_val is None:
192 continue
193 db_val = getattr(tour, col, None)
194 if db_val is None:
195 setattr(tour, col, hand_val)
196 elif col == 'koBounty':
197 setattr(tour, col, max(db_val, hand_val))
198 elif col == 'tourStartTime' and hand.startTime:
199 setattr(tour, col, min(db_val, hand.startTime))
201 if tour.entries is None and tour_type.sng:
202 tour.entries = tour_type.maxSeats
204 # fetch and update tourney players
205 for hp in self.handPlayers:
206 tp = TourneysPlayer.get_or_create(session, tour.id, hp.playerId)
207 # FIXME: other TourneysPlayers should be added here
209 session.flush()
211 def isDuplicate(self, session):
212 """Checks if current hand already exists in db
214 siteHandNo ans gametypeId have to be setted
216 return session.query(HandInternal).filter_by(
217 siteHandNo=self.siteHandNo, gametypeId=self.gametypeId).count()!=0
219 def __str__(self):
220 s = list()
221 for i in self._sa_class_manager.mapper.c:
222 s.append('%25s %s' % (i, getattr(self, i.name)))
224 s+=['', '']
225 for i,p in enumerate(self.handPlayers):
226 s.append('%d. %s' % (i, p.name or '???'))
227 s.append(str(p))
228 return '\n'.join(s)
230 @property
231 def boardcards(self):
232 cards = []
233 for i in range(5):
234 cards.append(getattr(self, 'boardcard%d' % (i+1), None))
235 return filter(bool, cards)
237 @property
238 def HandClass(self):
239 """Return HoldemOmahaHand or something like this"""
240 import Hand
241 if self.gametype.base == 'hold':
242 return Hand.HoldemOmahaHand
243 elif self.gametype.base == 'draw':
244 return Hand.DrawHand
245 elif self.gametype.base == 'stud':
246 return Hand.StudHand
247 raise Exception("Unknow gametype.base: '%s'" % self.gametype.base)
249 @property
250 def allStreets(self):
251 return self.HandClass.allStreets
253 @property
254 def actionStreets(self):
255 return self.HandClass.actionStreets
259 class HandPlayer(MappedBase):
260 """Class reflecting HandsPlayers db table"""
261 def __init__(self, **kwargs):
262 if 'imported_hand' in kwargs and 'seatNo' in kwargs:
263 imported_hand = kwargs.pop('imported_hand')
264 self.position = self.getPosition(imported_hand, kwargs['seatNo'])
265 super(HandPlayer, self).__init__(**kwargs)
267 @reconstructor
268 def init_on_load(self):
269 self.name = self.player.name
271 @staticmethod
272 def getPosition(hand, seat):
273 """Returns position value like 'B', 'S', '0', '1', ...
275 >>> class A(object): pass
276 ...
277 >>> A.noSb = False
278 >>> A.maxseats = 6
279 >>> A.buttonpos = 2
280 >>> A.gametype = {'base': 'hold'}
281 >>> A.players = [(i, None, None) for i in (2, 4, 5, 6)]
282 >>> HandPlayer.getPosition(A, 6) # cut off
284 >>> HandPlayer.getPosition(A, 2) # button
286 >>> HandPlayer.getPosition(A, 4) # SB
288 >>> HandPlayer.getPosition(A, 5) # BB
290 >>> A.noSb = True
291 >>> HandPlayer.getPosition(A, 5) # MP3
293 >>> HandPlayer.getPosition(A, 6) # cut off
295 >>> HandPlayer.getPosition(A, 2) # button
297 >>> HandPlayer.getPosition(A, 4) # BB
300 from itertools import chain
301 if hand.gametype['base'] == 'stud':
302 # FIXME: i've never played stud so plz check & del comment \\grindi
303 bringin = None
304 for action in chain(*[self.actions[street] for street in hand.allStreets]):
305 if action[1]=='bringin':
306 bringin = action[0]
307 break
308 if bringin is None:
309 raise Exception, "Cannot find bringin"
310 # name -> seat
311 bringin = int(filter(lambda p: p[1]==bringin, bringin)[0])
312 seat = (int(seat) - int(bringin))%int(hand.maxseats)
313 return str(seat)
314 else:
315 seats_occupied = sorted([seat_ for seat_, name, chips in hand.players], key=int)
316 if hand.buttonpos not in seats_occupied:
317 # i.e. something like
318 # Seat 3: PlayerX ($0), is sitting out
319 # The button is in seat #3
320 hand.buttonpos = max(seats_occupied,
321 key = lambda s: int(s)
322 if int(s) <= int(hand.buttonpos)
323 else int(s) - int(hand.maxseats)
325 seats_occupied = sorted(seats_occupied,
326 key = lambda seat_: (
327 - seats_occupied.index(seat_)
328 + seats_occupied.index(hand.buttonpos)
329 + 2) % len(seats_occupied)
331 # now (if SB presents) seats_occupied contains seats in order: BB, SB, BU, CO, MP3, ...
332 if hand.noSb:
333 # fix order in the case nosb
334 seats_occupied = seats_occupied[1:] + seats_occupied[0:1]
335 seats_occupied.insert(1, -1)
336 seat = seats_occupied.index(seat)
337 if seat == 0:
338 return 'B'
339 elif seat == 1:
340 return 'S'
341 else:
342 return str(seat-2)
344 @property
345 def cards(self):
346 cards = []
347 for i in range(7):
348 cards.append(getattr(self, 'card%d' % (i+1), None))
349 return filter(bool, cards)
351 def __str__(self):
352 s = list()
353 for i in self._sa_class_manager.mapper.c:
354 s.append('%45s %s' % (i, getattr(self, i.name)))
355 return '\n'.join(s)
358 class Site(object):
359 """Class reflecting Players db table"""
360 INITIAL_DATA = [
361 (1 , 'Full Tilt Poker','FT'),
362 (2 , 'PokerStars', 'PS'),
363 (3 , 'Everleaf', 'EV'),
364 (4 , 'Win2day', 'W2'),
365 (5 , 'OnGame', 'OG'),
366 (6 , 'UltimateBet', 'UB'),
367 (7 , 'Betfair', 'BF'),
368 (8 , 'Absolute', 'AB'),
369 (9 , 'PartyPoker', 'PP'),
370 (10, 'Partouche', 'PA'),
371 (11, 'Carbon', 'CA'),
372 (12, 'PKR', 'PK'),
374 INITIAL_DATA_KEYS = ('id', 'name', 'code')
376 INITIAL_DATA_DICTS = [ dict(zip(INITIAL_DATA_KEYS, datum)) for datum in INITIAL_DATA ]
378 @classmethod
379 def insert_initial(cls, connection):
380 connection.execute(sites_table.insert(), cls.INITIAL_DATA_DICTS)
383 class Tourney(MappedBase):
384 """Class reflecting Tourneys db table"""
386 @classmethod
387 def get_or_create(cls, session, siteTourneyNo, tourneyTypeId):
388 """Fetch tourney by index or creates one if none. """
389 return get_or_create(cls, session, siteTourneyNo=siteTourneyNo,
390 tourneyTypeId=tourneyTypeId)[0]
394 class TourneyType(MappedBase):
395 """Class reflecting TourneyType db table"""
397 @classmethod
398 def get_or_create(cls, session, **kwargs):
399 """Fetch tourney type by index or creates one if none
401 Required kwargs:
402 buyin fee speed maxSeats knockout
403 rebuy addOn shootout matrix sng currency
405 return get_or_create(cls, session, **kwargs)[0]
408 class TourneysPlayer(MappedBase):
409 """Class reflecting TourneysPlayers db table"""
411 @classmethod
412 def get_or_create(cls, session, tourneyId, playerId):
413 """Fetch tourney player by index or creates one if none """
414 return get_or_create(cls, session, tourneyId=tourneyId, playerId=playerId)
417 class Version(object):
418 """Provides read/write access for version var"""
419 CURRENT_VERSION = 120 # db version for current release
420 # 119 - first alchemy version
421 # 120 - add m_factor
423 conn = None
424 ver = None
425 def __init__(self, connection=None):
426 if self.__class__.conn is None:
427 self.__class__.conn = connection
429 @classmethod
430 def is_wrong(cls):
431 return cls.get() != cls.CURRENT_VERSION
433 @classmethod
434 def get(cls):
435 if cls.ver is None:
436 try:
437 cls.ver = cls.conn.execute(select(['version'], settings_table)).fetchone()[0]
438 except:
439 return None
440 return cls.ver
442 @classmethod
443 def set(cls, value):
444 if cls.conn.execute(settings_table.select()).rowcount==0:
445 cls.conn.execute(settings_table.insert(), version=value)
446 else:
447 cls.conn.execute(settings_table.update().values(version=value))
448 cls.ver = value
450 @classmethod
451 def set_initial(cls):
452 cls.set(cls.CURRENT_VERSION)
455 mapper (Gametype, gametypes_table, properties={
456 'hands': relation(HandInternal, backref='gametype'),
458 mapper (Player, players_table, properties={
459 'playerHands': relation(HandPlayer, backref='player'),
460 'playerTourney': relation(TourneysPlayer, backref='player'),
462 mapper (Site, sites_table, properties={
463 'gametypes': relation(Gametype, backref = 'site'),
464 'players': relation(Player, backref = 'site'),
465 'tourneyTypes': relation(TourneyType, backref = 'site'),
467 mapper (HandActions, hands_actions_table, properties={})
468 mapper (HandInternal, hands_table, properties={
469 'handPlayers': relation(HandPlayer, backref='hand'),
470 'actions_all': relation(HandActions, backref='hand', uselist=False),
472 mapper (HandPlayer, hands_players_table, properties={})
474 mapper (Tourney, tourneys_table)
475 mapper (TourneyType, tourney_types_table, properties={
476 'tourneys': relation(Tourney, backref='type'),
478 mapper (TourneysPlayer, tourneys_players_table)
480 class LambdaKeyDict(defaultdict):
481 """Operates like defaultdict but passes key argument to the factory function"""
482 def __missing__(key):
483 return self.default_factory(key)