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
22 #TODO: gettextify if file is used again
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"""
42 def get_or_create(session
, siteId
, name
):
43 return get_or_create(Player
, session
, siteId
=siteId
, name
=name
)[0]
46 return '<Player "%s" on %s>' % (self
.name
, self
.site
and self
.site
.name
)
49 class Gametype(MappedBase
):
50 """Class reflecting Gametypes db table"""
53 def get_or_create(session
, siteId
, gametype
):
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 ])
60 if gametype
['category'] in ('studhilo', 'omahahilo'):
62 elif gametype
['category'] in ('razz','27_3draw','badugi'):
64 gametype
['hiLo'] = hilo
66 for f
in ['smallBlind', 'bigBlind', 'smallBet', 'bigBet']:
67 if gametype
[f
] is None:
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
):
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
})
87 def flat_actions(self
):
89 for street
in self
.hand
.allStreets
:
90 actions
+= self
.actions
[street
]
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':
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:
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"""
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
)
164 hand
.buyin
, self
.fee
= m
.groups()
167 tour_type_hand2db
= {
171 'maxSeats': 'maxseats',
175 'shootout': 'isShootout',
176 'matrix': 'isMatrix',
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()
190 hand_val
= getattr(hand
, col
, None)
191 if col
in ('id', 'tourneyTypeId', 'comment', 'commentTs') or hand_val
is None:
193 db_val
= getattr(tour
, col
, 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
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
221 for i
in self
._sa
_class
_manager
.mapper
.c
:
222 s
.append('%25s %s' % (i
, getattr(self
, i
.name
)))
225 for i
,p
in enumerate(self
.handPlayers
):
226 s
.append('%d. %s' % (i
, p
.name
or '???'))
231 def boardcards(self
):
234 cards
.append(getattr(self
, 'boardcard%d' % (i
+1), None))
235 return filter(bool, cards
)
239 """Return HoldemOmahaHand or something like this"""
241 if self
.gametype
.base
== 'hold':
242 return Hand
.HoldemOmahaHand
243 elif self
.gametype
.base
== 'draw':
245 elif self
.gametype
.base
== 'stud':
247 raise Exception("Unknow gametype.base: '%s'" % self
.gametype
.base
)
250 def allStreets(self
):
251 return self
.HandClass
.allStreets
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
)
268 def init_on_load(self
):
269 self
.name
= self
.player
.name
272 def getPosition(hand
, seat
):
273 """Returns position value like 'B', 'S', '0', '1', ...
275 >>> class A(object): pass
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
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
304 for action
in chain(*[self
.actions
[street
] for street
in hand
.allStreets
]):
305 if action
[1]=='bringin':
309 raise Exception, "Cannot find bringin"
311 bringin
= int(filter(lambda p
: p
[1]==bringin
, bringin
)[0])
312 seat
= (int(seat
) - int(bringin
))%int
(hand
.maxseats
)
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, ...
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
)
348 cards
.append(getattr(self
, 'card%d' % (i
+1), None))
349 return filter(bool, cards
)
353 for i
in self
._sa
_class
_manager
.mapper
.c
:
354 s
.append('%45s %s' % (i
, getattr(self
, i
.name
)))
359 """Class reflecting Players db table"""
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'),
374 INITIAL_DATA_KEYS
= ('id', 'name', 'code')
376 INITIAL_DATA_DICTS
= [ dict(zip(INITIAL_DATA_KEYS
, datum
)) for datum
in INITIAL_DATA
]
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"""
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"""
398 def get_or_create(cls
, session
, **kwargs
):
399 """Fetch tourney type by index or creates one if none
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"""
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
425 def __init__(self
, connection
=None):
426 if self
.__class
__.conn
is None:
427 self
.__class
__.conn
= connection
431 return cls
.get() != cls
.CURRENT_VERSION
437 cls
.ver
= cls
.conn
.execute(select(['version'], settings_table
)).fetchone()[0]
444 if cls
.conn
.execute(settings_table
.select()).rowcount
==0:
445 cls
.conn
.execute(settings_table
.insert(), version
=value
)
447 cls
.conn
.execute(settings_table
.update().values(version
=value
))
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
)