# PyBridge -- online contract bridge made easy.
# Copyright (C) 2004-2007 PyBridge Project.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# 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 General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""
This module provides two-way conversion between BridgeGame objects and the
PBN 2.0 (Portable Bridge Notation) format.
The PBN 2.0 specification is available from http://www.tistis.nl/pbn/.
"""
import re
import time
from card import Card
from deck import Deck
from call import Bid, Pass, Double, Redouble
from symbols import Player as Position, Level, Strain, Rank, Suit, Vulnerable
class ParseError(Exception):
"""Raised when PBN parser encounters an unexpected input."""
def importPBN(pbn, complete=True):
"""Builds a BridgeGame object from a given PBN "import"-format string.
@param pbn: a string containing PBN markup.
@param complete: if True, expect the game described in pbn to be complete.
Raises a ParseError if BridgeGame cannot be completed.
@return: a BridgeGame object equivalent to the PBN string.
"""
from newnewgame import BridgeGame, GameError
# Mappings from PBN symbols to their PyBridge type equivalents.
BIDLEVEL = dict(zip('1234567', Level))
BIDSTRAIN = dict(zip('CDHS', Strain) + [('NT', Strain.NoTrump)])
CARDRANK = dict(zip('23456789TJQKA', Rank))
CARDSUIT = dict(zip('CDHS', Suit))
POSITION = dict(zip('NESW', Position))
VULN = {'All': Vulnerable.All, 'Both': Vulnerable.All, '-': Vulnerable.None,
'NS': Vulnerable.NorthSouth, 'EW': Vulnerable.EastWest,
'None': Vulnerable.None, 'Love': Vulnerable.None}
game = BridgeGame()
players = dict([(pos, game.addPlayer(pos)) for pos in Position])
tags, sections, notes = parsePBN(pbn)
board = {}
# Load values of non-essential tags.
board['event'] = tags.get('Event')
board['site'] = tags.get('Site')
if tags.get('Date'): # Convert to time tuple.
board['date'] = time.strptime(tags['Date'], "%Y.%m.%d")
board['boardnum'] = int(tags.get('Board', 0))
board['players'] = dict([(p, tags.get(p.key)) for p in Position])
# Load values of essential tags.
for tag in ('Dealer', 'Vulnerable', 'Deal', 'Auction', 'Play'):
if tag not in tags:
raise ParseError, "Required tag '%s' not found" % tag
try:
board['dealer'] = POSITION[tags['Dealer']]
board['vulnerable'] = VULN[tags['Vulnerable']]
# Reconstruct deal.
board['deal'] = {}
first, hands = tags['Deal'].split(":")
firstindex = POSITION[first.strip()].index
order = Position[firstindex:] + Position[:firstindex]
for player, hand in zip(order, hands.strip().split()):
board['deal'][player] = []
for suit, suitcards in zip(reversed(Suit), hand.split('.')):
for rank in suitcards:
card = Card(CARDRANK[rank], suit)
board['deal'][player].append(card)
# Validate deal.
deck = Deck()
if not deck.isValidDeal(board['deal']):
raise ParseError, "Deal does not validate"
game.start(board) # Initialise game with board.
# Process Auction section: build bidding.
# TODO: PBN does not need to provide an auction.
pos = POSITION[tags['Auction']]
for item in sections['Auction'].split():
if item.startswith('='): # A note identifier.
continue
elif item.startswith('-'): # Skip position.
pos = Position[(pos.index + 1) % len(Position)] # Next player.
continue
# Extract call from item.
if item[0] in BIDLEVEL and item[1:] in BIDSTRAIN:
call = Bid(BIDLEVEL[item[0]], BIDSTRAIN[item[1:]])
elif item == 'Pass':
call = Pass()
elif item == 'X':
call = Double()
elif item == 'XX':
call = Redouble()
else:
raise ParseError, "Unrecognised item '%s' in Auction" % item
try: # Make call.
players[pos].makeCall(call)
pos = Position[(pos.index + 1) % len(Position)] # Next player.
except GameError, err:
raise ParseError, "Invalid call %s in Auction: %s" % (call, err)
# Process Play section: build play.
first = POSITION[tags['Play']]
for line in sections['Play'].splitlines():
leader, cards = game.getTurn(), {} # Trick.
# Extract cards from line.
for item in line.split():
print item
if item.startswith('='): # A note identifier.
continue
if item[0] in CARDSUIT and item[1] in CARDRANK:
card = Card(CARDRANK[item[1]], CARDSUIT[item[0]])
cards[Position[(first.index + len(cards)) % len(Position)]] = card
else:
raise ParseError, "Unrecognised item '%s' in Play" % item
try: # Play cards in trick.
for pos in Position[leader.index:] + Position[:leader.index]:
players[pos].playCard(card)
except GameError, err:
raise ParseError, "Invalid card %s in Play: %s" % (card, err)
except KeyError, key:
raise ParseError, "Invalid value %s for attribute" % key
def exportPBN(game):
"""Builds a PBN "export"-format string from a given BridgeGame object.
@param game: a BridgeGame object.
@return: a PBN string equivalent to the BridgeGame object.
"""
# Mappings from PyBridge symbol types to their PBN equivalents.
RANKS = dict(zip(Rank, "23456789TJQKA"))
SUITS = dict(zip(Suit, "CDHS"))
POSITIONS = dict(zip(Player, "NESW"))
'''
def importPBN(self, pbn):
"""Builds a BridgeGame object from a given PBN "import format" string.
@param pbn: a string containing PBN markup.
@return: a BridgeGame object equivalent to the PBN string.
"""
# This lambda reverses the mapping between keys and values of dict d.
# It assumes there are no duplicate values in d.
invert = lambda d: dict([(v, k) for k, v in d.iteritems()])
tagValues, sectionData = self.parse(pbn)
# Get dealer.
dealer = invert(self.SEATS)[tagValues['Dealer']]
# Get deal.
deal = {}
# Determine first hand in Deal string.
first = invert(self.SEATS)[tagValues['Deal'][0]]
seatorder = Seat[first.index:] + Seat[:first.index]
# Split deal into hands, into suits, into cards.
handstrings = tagValues['Deal'][2:].split(' ')
for seat, handstring in zip(seatorder, handstrings):
deal[seat] = []
for suit, rankstring in zip(self.SUITORDER, handstring.split('.')):
for rankchar in rankstring:
card = Card(invert(self.RANKS)[rankchar], suit)
deal[seat].append(card)
# Get vulnerability.
for vulnTuple, vulnTexts in self.VULNERABLE.items():
# Since PBN allows variations on the Vulnerable tag.
if tagValues['Vulnerable'] in vulnTexts:
vulnNS, vulnEW = vulnTuple
break
scoring = scoreDuplicate # For now, just use duplicate scoring method.
game = Game(dealer, deal, scoring, vulnNS, vulnEW)
#
# TODO - determine the calls made, load them in with game.makeCall
# - determine the cards played, load them in with game.playCard
#
# Finally, return the BridgeGame object.
return game
def exportPBN(self, game):
"""Builds a PBN "export format" string from a given BridgeGame object.
@param game: a BridgeGame object.
@return: a PBN string equivalent to the BridgeGame object.
"""
def makeTag(key, value):
"""A convenience function to generate a PBN-style tag."""
return '[%s \"%s\"]\n' % (key, value)
pbn = ''
#
# TODO: fill out all 15 fields, with respect to PBN spec.
#
# (1) Event (the name of the tournament or match)
pbn += makeTag('Event', 'Unknown')
# (2) Site (the location of the event)
pbn += makeTag('Site', 'Unknown')
# (3) Date (the starting date of the game)
# TODO: use the localtime() method of the time module
pbn += makeTag('Date', '%s.%s.%s' % (1,2,3))
# (4) Board (the board number)
pbn += makeTag('Board', '1')
# (5) West (the west player)
# (6) North (the north player)
# (7) East (the east player)
# (8) South (the south player)
# TODO: use 'Unknown' tags
# (9) Dealer (the dealer)
pbn += makeTag('Dealer', self.SEATS[game.bidding.dealer])
# (10) Vulnerable (the situation of vulnerability)
# TODO: put game.vulnNS and game.vulnEW into a tuple, use tuple as index on self.VULNERABLE,
# get first value (the [0]) from list.
# pbn += makeTag('Vulnerable', self.VULNERABLE[( , )][0]
# (11) Deal (the dealt cards)
# (12) Scoring (the scoring method)
# (13) Declarer (the declarer of the contract)
# (14) Contract (the contract)
# (15) Result (the result of the game)
return pbn
'''
def parsePBN(pbn):
"""Parses the given PBN string and extracts:
* for each PBN tag, a dict of associated key/value pairs.
* for each data section, a dict of key/data pairs.
This method does not interpret the PBN string itself.
@param pbn: a string containing PBN markup.
@return: a tuple (tag values, section data, notes).
"""
tagValues, sectionData, notes = {}, {}, {}
for line in pbn.splitlines():
line.strip() # Remove whitespace.
if line.startswith('%'): # A comment.
pass # Skip over comments.
elif line.startswith('['): # A tag.
line = line.strip('[]') # Remove leading [ and trailing ].
# The key is the first word, the value is everything after.
tag, value = line.split(' ', 1)
tag = tag.capitalize()
value = value.strip('\'\"')
if tag == 'Note':
notes.setdefault(tag, [])
notes[tag].append(value)
else:
tagValues[tag] = value
else: # Line follows tag, add line to data buffer for section.
sectionData.setdefault(tag, '')
sectionData[tag] += line + '\n'
return tagValues, sectionData, notes
def testImport():
pbnstr = file("a-pbn-file.pbn").read() # Set path as appropriate.
pbn = PBNHandler() # Create our handler.
g = pbn.importPBN(pbnstr) # Do it!
return g