summaryrefslogtreecommitdiff
path: root/src/bcdd/ParScore.py
diff options
context:
space:
mode:
authorMichał Klichowicz <emkael@tlen.pl>2023-09-30 12:40:40 +0200
committerMichał Klichowicz <emkael@tlen.pl>2023-09-30 12:40:40 +0200
commit518bdba985a913044e84e82713a8e76f5ddd3301 (patch)
tree6757ec4d785c07e2c3f34e70b5bd7326fe7c7a2c /src/bcdd/ParScore.py
Initial import script
Diffstat (limited to 'src/bcdd/ParScore.py')
-rw-r--r--src/bcdd/ParScore.py171
1 files changed, 171 insertions, 0 deletions
diff --git a/src/bcdd/ParScore.py b/src/bcdd/ParScore.py
new file mode 100644
index 0000000..e23a839
--- /dev/null
+++ b/src/bcdd/ParScore.py
@@ -0,0 +1,171 @@
+import re
+
+from .BCalcWrapper import BCalcWrapper as bcw
+from .ParContract import ParContract
+from .Exceptions import FieldNotFoundException
+
+
+class ParScore(object):
+ _pbn_contract_pattern = re.compile(r'(\d)([CDHSN])(X?)\s+([NESW])')
+ _pbn_score_pattern = re.compile(r'(NS|EW)\s+(-?\d})')
+ _jfr_contract_pattern = re.compile(r'^(\d)([CDHSN])(D?)([NESW])(-?\d+)$')
+
+ def __init__(self, board):
+ self._board = board
+
+ def get_pbn_par_contract(self):
+ contract_field = self._board.get_optimum_result()
+ if 'Pass' == contract_field:
+ return ParContract()
+ contract_match = self._pbn_contract_pattern.match(contract_field)
+ if not contract_match:
+ raise ParScoreInvalidException(
+ 'Invalid format for OptimumResult field: ' + contract_field)
+ score_field = self._board.get_optimum_score()
+ score_match = self._pbn_score_pattern.match(score_field)
+ if not score_match:
+ raise ParScoreInvalidException(
+ 'Invalid format for OptimumScore field: ' + scoreField)
+ score = int(score_match.group(2))
+ if 'EW' == score_match.group(1):
+ score = -score
+ contract = ParContract(
+ int(contract_match.group(1)),
+ contract_match.group(2)[0],
+ contract_match.group(4)[0],
+ 'X' == contract_match.group(3),
+ score)
+ return contract.validate()
+
+ def get_jfr_par_contract(self):
+ par_string = self._board.get_minimax()
+ par_match = self._jfr_contract_pattern.match(par_string)
+ if not par_match:
+ raise ParScoreInvalidException(
+ 'Invalid format for Minimax field: ' + par_string)
+ if '0' == par_match.group(4):
+ return ParContract() # pass-out
+ contract = ParContract(
+ int(par_match.group(1)),
+ par_match.group(2)[0],
+ par_match.group(4)[0],
+ 'D' == par_match.group(3),
+ int(par_match.group(5)))
+ return contract.validate()
+
+ def _determine_vulnerability(self, vulnerability, declarer):
+ vulnerability = vulnerability.upper()
+ return vulnerability in ['ALL', 'BOTH'] \
+ or (vulnerability not in ['LOVE', 'NONE'] \
+ and declarer in vulnerability)
+
+ def _get_highest_makeable_contract(self, dd_table,
+ for_ns=True, for_ew=True):
+ contract = ParContract()
+ tricks = 0
+ for i in range(3, -1, -1):
+ if ((i % 2 == 0) and for_ns) \
+ or ((i % 2 == 1) and for_ew):
+ for j in range(0, 5):
+ level = dd_table[i][j] - 6
+ denomination = bcw.DENOMINATIONS.index(
+ contract.denomination) \
+ if contract.denomination in bcw.DENOMINATIONS \
+ else -1
+ if (level > contract.level) \
+ or ((level == contract.level) \
+ and (j > denomination)):
+ contract.level = level
+ contract.denomination = bcw.DENOMINATIONS[j]
+ contract.declarer = bcw.PLAYERS[i]
+ tricks = dd_table[i][j]
+ vulnerability = self._board.get_vulnerable().upper()
+ vulnerable = self._determine_vulnerability(
+ vulnerability, contract.declarer)
+ contract.score = contract.calculate_score(tricks, vulnerable)
+ return contract
+
+ def get_dd_table_par_contract(self, dd_table):
+ dealer = self._board.get_dealer()
+ vulnerability = self._board.get_vulnerable().upper()
+ ns_highest = self._get_highest_makeable_contract(
+ dd_table, True, False);
+ ew_highest = self._get_highest_makeable_contract(
+ dd_table, False, True)
+ if ns_highest == ew_highest:
+ return ns_highest.validate() \
+ if dealer in ['N', 'S'] else ew_highest.validate()
+ highest = max(ns_highest, ew_highest)
+ other_side_highest = min(ew_highest, ns_highest)
+ ns_playing = highest.declarer in ['N', 'S']
+ defense_vulnerability = self._determine_vulnerability(
+ vulnerability, 'E' if ns_playing else 'N')
+ highest_defense = highest.get_defense(dd_table, defense_vulnerability)
+ if highest_defense is not None:
+ # Highest contract has profitable defense
+ return highest_defense.validate()
+ denomination_index = bcw.DENOMINATIONS.index(highest.denomination) \
+ if highest.denomination in bcw.DENOMINATIONS \
+ else -1
+ declarer_index = bcw.PLAYERS.index(highest.declarer) \
+ if highest.declarer in bcw.PLAYERS else -1
+ player_indexes = [declarer_index, (declarer_index + 2) % 4]
+ vulnerable = self._determine_vulnerability(
+ vulnerability, highest.declarer)
+ score_squared = highest.score * highest.score
+ possible_optimums = []
+ for i in range(0, 5):
+ for player in player_indexes:
+ level = highest.level
+ if i > denomination_index:
+ level -= 1
+ while level > 0:
+ contract = ParContract(
+ level,
+ bcw.DENOMINATIONS[i],
+ bcw.PLAYERS[player],
+ False, 0)
+ contract.score = contract.calculate_score(
+ dd_table[player][i], vulnerable)
+ if other_side_highest > contract:
+ # Contract is lower than other side's contract
+ break
+ if (highest.score * contract.score) > 0:
+ # Contract makes
+ if abs(contract.score) >= abs(highest.score):
+ # Contract is profitable
+ defense = contract.get_defense(
+ dd_table, defense_vulnerability)
+ if defense is not None \
+ and (contract.score * contract.score) \
+ > (contract.score * defense.score):
+ # Contract has defense
+ possible_optimums.append(defense)
+ # So lower contracts will too.
+ break
+ else:
+ # Contract does not have defense
+ possible_optimums.append(contract)
+ else:
+ # Contract is not profitable
+ break
+ level -= 1
+ for contract in possible_optimums:
+ if abs(contract.score) > abs(highest.score):
+ # Contract is more profitable
+ highest = contract
+ else:
+ if contract.score == highest.score:
+ if highest > contract:
+ # Equally profitable, but lower
+ highest = contract
+ return highest.validate()
+
+ def get_par_contract(self, dd_table):
+ try:
+ return self.get_jfr_par_contract()
+ except FieldNotFoundException:
+ try:
+ return self.get_pbn_par_contract()
+ except FieldNotFoundException:
+ return self.get_dd_table_par_contract(dd_table)