From: <umg...@us...> - 2007-07-20 14:32:17
|
Revision: 485 http://svn.sourceforge.net/pybridge/?rev=485&view=rev Author: umgangee Date: 2007-07-20 07:32:16 -0700 (Fri, 20 Jul 2007) Log Message: ----------- New Deal class replaces Deck. Deck-specific methods become Deal classmethods. Use 1..D range instead of 0..D-1 for consistency with IBB. (although the IBB algorithm itself deals in ranges 0..D-1) Modified Paths: -------------- trunk/pybridge/pybridge/games/bridge/board.py Added Paths: ----------- trunk/pybridge/pybridge/games/bridge/deal.py trunk/pybridge/tests/bridge/test_deal.py Removed Paths: ------------- trunk/pybridge/pybridge/games/bridge/deck.py trunk/pybridge/tests/bridge/test_deck.py Modified: trunk/pybridge/pybridge/games/bridge/board.py =================================================================== --- trunk/pybridge/pybridge/games/bridge/board.py 2007-07-19 15:30:30 UTC (rev 484) +++ trunk/pybridge/pybridge/games/bridge/board.py 2007-07-20 14:32:16 UTC (rev 485) @@ -18,7 +18,7 @@ import random import time -from deck import Deck +from deal import Deal from symbols import Direction, Vulnerable @@ -53,8 +53,7 @@ @param result: @type result: """ - deck = Deck() - self['deal'] = deck.randomDeal() + self['deal'] = Deal.fromRandom() self['num'] = self.get('num', 0) + 1 self['time'] = tuple(time.localtime()) Copied: trunk/pybridge/pybridge/games/bridge/deal.py (from rev 480, trunk/pybridge/pybridge/games/bridge/deck.py) =================================================================== --- trunk/pybridge/pybridge/games/bridge/deal.py (rev 0) +++ trunk/pybridge/pybridge/games/bridge/deal.py 2007-07-20 14:32:16 UTC (rev 485) @@ -0,0 +1,184 @@ +# 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. + + +from copy import copy +from operator import mul +import random + +from card import Card +from symbols import Direction, Rank, Suit + + +# See http://mail.python.org/pipermail/edu-sig/2001-May/001288.html for details. +comb = lambda n, k: reduce(mul, range(n, n-k, -1)) / reduce(mul, range(1, k+1)) + + +class Deal(dict): + """Represents a deal of hands as a dict containing lists of cards. + + Operations to encode/decode deals into a variety of formats are provided, + including "impossible bridge book" index values and PBN strings. + + Definitions: + - A hand is a collection of 13 cards from the deck. + - A deal is a distribution of all 52 cards to four hands. + + There are exactly 52! / (13!)**4 (comb(52,13) * comb(39,13) * comb(26,13)) + distinct deals of 13 cards to 4 positions from a standard 52-card deck. + """ + + # Required order: Ace of Spades, King of Spades, ..., Two of Clubs. + __cards = [Card(r, s) for s in reversed(Suit) for r in reversed(Rank)] + + __Nmax = comb(52, 13) + __Emax = comb(39, 13) + __Smax = comb(26, 13) + __D = __Nmax * __Emax * __Smax + + + def __init__(self, mapping): + super(Deal, self).__init__(mapping) + for hand in self.values(): + hand.sort() + + + @classmethod + def fromRandom(cls): + """Generates a random deal of hands from a 'shuffled' deck. + + @return: an instance of Deal. + """ + # This is more efficient than calling fromIndex() with a random number. + deck = copy(cls.__cards) + random.shuffle(deck) + hands = dict((pos, sorted(deck[13*i : 13*(i+1)])) + for i, pos in enumerate(Direction)) + return cls(hands) + + + @classmethod + def fromIndex(cls, num): + """Generates the deal which corresponds to the specified "page number". + + This implements the "impossible bridge book" decoding algorithm by + Thomas Andrews, see http://bridge.thomasoandrews.com/impossible/. + + @param num: integer in range 1..D. + @return: a Deal object containing the corresponding deal. + """ + assert isinstance(num, (int, long)), "index must be an integer" + assert 1 <= num <= cls.__D, "index not in range %s..%s" % (1, cls.__D) + + cardSeq = copy(cls.__cards) # Make a copy for modification. + hands = dict((pos, []) for pos in Direction) + num -= 1 # Decrement page number to fit within range 0..D-1. + + # Split index into hand indexes. + indexes = {Direction.North : (num / cls.__Smax) / cls.__Emax, + Direction.East : (num / cls.__Smax) % cls.__Emax, + Direction.South : (num % cls.__Smax) } + + for position in (Direction.North, Direction.East, Direction.South): + for k in range(13, 0, -1): + + # Find the largest n such that comb(n, k) <= indexes[position]. + n = k-1 # n < k implies comb(n, k) = 0 + # comb(n+1, k) = + # n-k == -1 => comb(n, k) * (n+1) + # otherwise => (comb(n, k) * (n+1)) / (n+1 - k) + while comb(n+1, k) <= indexes[position]: + n += 1 + + # Remove card index from indices, add card to hand. + indexes[position] -= comb(n, k) + card = cardSeq[n] + hands[position].append(card) + cardSeq.remove(card) + + hands[Direction.West] = cardSeq # West has the remaining cards. + + return cls(hands) + + + def toIndex(self): + """Computes the "page number" which corresponds to this deal. + + This implements the "impossible bridge book" encoding algorithm by + Thomas Andrews, see http://bridge.thomasoandrews.com/impossible/. + + @return: integer in range 1..D + """ + cardSeq = copy(self.__cards) # Make a copy for modification. + indexes = {} + + # For each hand, compute indexes of cards in cardSeq. + for position in (Direction.North, Direction.East, Direction.South): + indexes[position] = 0 + self[position].sort(reverse=False) + # It is desirable to remove cards from cardSeq when adding their + # indexes, instead of doing so in an extra step. + # Removing cards backwards preserves the indexes of later cards. + for i, card in enumerate(self[position]): + indexes[position] += comb(cardSeq.index(card), 13-i) + cardSeq.remove(card) + + # Deal index = (Nindex * Emax * Smax) + (Eindex * Smax) + Sindex + indexes[Direction.North] *= self.__Emax * self.__Smax + indexes[Direction.East] *= self.__Smax + + num = sum(indexes.values()) + 1 # Increment to fit within range 1..D. + return long(num) + + + __pbnDirection = dict(zip('NESW', Direction)) + __pbnRank = dict(zip('23456789TJQKA', Rank)) + + + @classmethod + def fromString(cls, dealstr): + """Generates the deal which corresponds to the given PBN deal string. + + As per the PBN specification, the given deal string should conform to + the format "<first>:<1st_hand> <2nd_hand> <3rd_hand> <4th_hand>". + + @param dealstr: a PBN deal string. + @return: a Deal object containing the corresponding deal. + """ + # Reconstruct deal. + first, hands = dealstr.split(":") + firstindex = cls.__pbnDirection[first.strip()].index + order = Direction[firstindex:] + Direction[:firstindex] + + deal = dict((pos, []) for pos in Direction) + + for position, hand in zip(order, hands.strip().split(' ')): + for suit, suitcards in zip(reversed(Suit), hand.split('.')): + for rank in suitcards: + card = Card(cls.__pbnRank[rank], suit) + deal[position].append(card) + + return cls(deal) + + + def toString(self): + """Computes the PBN deal string which corresponds to this deal. + + @return: a PBN deal string. + """ + raise NotImplementedError + Deleted: trunk/pybridge/pybridge/games/bridge/deck.py =================================================================== --- trunk/pybridge/pybridge/games/bridge/deck.py 2007-07-19 15:30:30 UTC (rev 484) +++ trunk/pybridge/pybridge/games/bridge/deck.py 2007-07-20 14:32:16 UTC (rev 485) @@ -1,157 +0,0 @@ -# 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. - - -from copy import copy -from operator import mul -from random import shuffle - -from card import Card -from symbols import Direction, Rank, Suit - - -# See http://mail.python.org/pipermail/edu-sig/2001-May/001288.html for details. -comb = lambda n, k: reduce(mul, range(n, n-k, -1)) / reduce(mul, range(1, k+1)) - - -# TODO: consider making Hand a subclass of List, with additional constraints. - -class Deck(object): - """A Deck object provides operations for dealing Card objects. - - A hand is a collection of 13 cards from the deck. - A deal is a distribution of all 52 cards to four hands. - - A deal is represented as a dictionary, mapping Direction labels to - lists (hands) of Card objects. - - There are exactly 52! / (13!)**4 (comb(52,13) * comb(39,13) * comb(26,13)) - distinct deals of 13 cards to 4 positions from a standard 52-card deck. - """ - - cards = [Card(r, s) for r in Rank for s in Suit] - cardSeq = copy(cards) - cardSeq.sort(reverse=True) # Required order: Ace of Spades -> Two of Clubs. - - Nmax = comb(52, 13) - Emax = comb(39, 13) - Smax = comb(26, 13) - D = Nmax * Emax * Smax - - - def isValidDeal(self, deal): - """Checks that structure of deal conforms to requirements: - - * 4-element dict, mapping Direction objects to hand lists. - * Hand lists contain exactly 13 Card objects. - * No card may be repeated in the same hand, or between hands. - * The cards in hands may be in any order. - - @param deal: a deal dict. - @return: True if deal is valid, False otherwise. - """ - return True # TODO - if invalid, perhaps give reason - - - def randomDeal(self): - """Shuffles the deck and generates a random deal of hands. - - @return: a deal dictionary. - """ - shuffle(self.cards) - hands = {} - for position in Direction: - hands[position] = [] - for index, card in enumerate(self.cards): - hands[Direction[index % len(Direction)]].append(card) - for hand in hands.values(): - hand.sort() - return hands - - - def dealToIndex(self, deal): - """Computes the index which corresponds to the specified deal. - - This implements the "impossible bridge book" encoding algorithm by - Thomas Andrews, see http://bridge.thomasoandrews.com/impossible/. - - @param deal: dict representing a valid deal. - @return: integer in range 0..D-1 - """ - assert self.isValidDeal(deal) - - cardSeq = copy(self.cardSeq) # Make a copy for modification. - indexes = {} - - # For each hand, compute indexes of cards in cardSeq. - for position in (Direction.North, Direction.East, Direction.South): - indexes[position] = 0 - deal[position].sort(reverse=False) - # It is desirable to remove cards from cardSeq when adding their - # indexes, instead of doing so in an extra step. - # Removing cards backwards preserves the indexes of later cards. - for i, card in enumerate(deal[position]): - indexes[position] += comb(cardSeq.index(card), 13-i) - cardSeq.remove(card) - - # Deal index = (Nindex * Emax * Smax) + (Eindex * Smax) + Sindex - indexes[Direction.North] *= self.Emax * self.Smax - indexes[Direction.East] *= self.Smax - return long(sum(indexes.values())) - - - def indexToDeal(self, num): - """Generates the deal which corresponds to the specified index. - - This implements the "impossible bridge book" decoding algorithm by - Thomas Andrews, see http://bridge.thomasoandrews.com/impossible/. - - @param num: integer in range 0..D-1. - @return: dict representing a valid deal. - """ - assert type(num) in (int, long), "index must be an integer" - assert 0 <= num < self.D, "index not in required range" - - cardSeq = copy(self.cardSeq) # Make a copy for modification. - deal = {} - - # Split index into hand indexes. - indexes = {Direction.North : (num / self.Smax) / self.Emax, - Direction.East : (num / self.Smax) % self.Emax, - Direction.South : (num % self.Smax) } - - for position in (Direction.North, Direction.East, Direction.South): - deal[position] = [] - for k in range(13, 0, -1): - # Find the largest n such that comb(n, k) <= indexes[position]. - n = k-1 # n < k implies comb(n, k) = 0 - - # comb(n+1, k) = - # n-k = -1 => comb(n, k) * (n+1) - # otherwise => (comb(n, k) * (n+1)) / (n+1 - k) - while comb(n+1, k) <= indexes[position]: - n += 1 - - # Remove card index from indices, add card to hand. - indexes[position] -= comb(n, k) - card = cardSeq[n] - deal[position].append(card) - cardSeq.remove(card) - - deal[Direction.West] = cardSeq # West has the remaining cards. - return deal - Copied: trunk/pybridge/tests/bridge/test_deal.py (from rev 480, trunk/pybridge/tests/bridge/test_deck.py) =================================================================== --- trunk/pybridge/tests/bridge/test_deal.py (rev 0) +++ trunk/pybridge/tests/bridge/test_deal.py 2007-07-20 14:32:16 UTC (rev 485) @@ -0,0 +1,83 @@ +import unittest + +from pybridge.games.bridge.card import Card +from pybridge.games.bridge.deal import Deal +from pybridge.games.bridge.symbols import Direction, Rank, Suit + + +class TestDeck(unittest.TestCase): + + cards = sorted(Card(r, s) for r in Rank for s in Suit) + + samples = {} + + samples[1] = Deal({Direction.North: [Card(r, Suit.Spade) for r in Rank], + Direction.East: [Card(r, Suit.Heart) for r in Rank], + Direction.South: [Card(r, Suit.Diamond) for r in Rank], + Direction.West: [Card(r, Suit.Club) for r in Rank]}) + + # http://bridgehands.com/D/Duke_of_Cumberland_Hand.htm + samples[25216995119420903953708290155] = Deal.fromString( + "N:..Q8765432.AQT84 65432.T9872.JT9. T987.6543..76532 AKQJ.AKQJ.AK.KJ9") + + # http://bridgehands.com/B/John_Bennett_Murder.htm + samples[49115408832893597588305377049] = Deal.fromString( + "S:KJ985.K762.85.KT Q72.AJ3.AQT92.J6 AT63.T85.4.A9842 4.Q94.KJ763.Q753") + + # From the PBN v2.0 specification document. + samples[51845212465382378289082480212] = Deal.fromString( + "N:.63.AKQ987.A9732 A8654.KQ5.T.QJT6 J973.J98742.3.K4 KQT2.AT.J6542.85") + + # http://bridgehands.com/M/Mississippi_Heart_Hand.htm + samples[53520933857671775260919265981] = Deal.fromString( + "S:AKQ.AKQJT9..AKQJ .8765432.AKQJT9. T5432..5432.5432 J9876..876.T9876") + + + def validateDeal(self, deal): + """Checks that structure of deal conforms to requirements: + + - Each position in Direction maps to a hand, represented as a list. + - Hand lists contain exactly 13 Card objects. + - No card may be repeated in the same hand, or between hands. + + @param deal: a Deal instance. + """ + assert isinstance(deal, Deal), "deal not a Deal instance" + assert set(deal.keys()) == set(Direction), "invalid set of keys" + + extractcards = [] + for pos, hand in deal.items(): + assert len(hand) == 13, "%s hand does not contain 13 cards" % pos + extractcards.extend(hand) + assert self.cards == sorted(extractcards), "not a pure set of cards" + + + def test_generateRandom(self): + """Testing generation of random deals""" + deal = Deal.fromRandom() + try: + self.validateDeal(deal) + except Exception, e: + self.fail(e, deal) + + + def test_toIndex(self): + """Testing toIndex method over a set of known deals""" + for index, deal in self.samples.items(): + self.assertEqual(deal.toIndex(), index) + + + def test_fromIndex(self): + """Testing Deal.fromIndex over a set of known indexes""" + for index, deal in self.samples.items(): + self.assertEqual(Deal.fromIndex(index), deal) + + +def main(): + suite = unittest.makeSuite(TestDeck) + unittest.TextTestRunner(verbosity=2).run(suite) + + +if __name__ == '__main__': + main() + Deleted: trunk/pybridge/tests/bridge/test_deck.py =================================================================== --- trunk/pybridge/tests/bridge/test_deck.py 2007-07-19 15:30:30 UTC (rev 484) +++ trunk/pybridge/tests/bridge/test_deck.py 2007-07-20 14:32:16 UTC (rev 485) @@ -1,52 +0,0 @@ -import unittest - -from pybridge.games.bridge.deck import Deck - - -class TestDeck(unittest.TestCase): - - - def setUp(self): - self.deck = Deck() - - - def tearDown(self): - self.deck = None - - - def testRandomDeal(self): - """Testing randomDeal""" - for i in range(100): - deal = self.deck.randomDeal() - self.assert_(self.deck.isValidDeal(deal)) - - - def testDealToIndex(self): - """Testing dealToIndex (assuming indexToDeal correct)""" - n = 1 - while n < self.deck.D: - deal = self.deck.indexToDeal(n) - pn = self.deck.dealToIndex(deal) - self.assertEqual(n, pn) - n = n*2 + 1 - - - def testIndexToDeal(self): - """Testing indexToDeal (assuming dealToIndex and isValidDeal correct)""" - n = 1 - while n < self.deck.D: - deal = self.deck.indexToDeal(n) - self.assert_(self.deck.isValidDeal(deal)) - pn = self.deck.dealToIndex(deal) - self.assertEqual(n, pn) - n = n*2 + 1 - - -def main(): - suite = unittest.makeSuite(TestDeck) - unittest.TextTestRunner(verbosity=2).run(suite) - - -if __name__ == '__main__': - main() - This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |