summaryrefslogtreecommitdiff
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
Initial import script
-rw-r--r--src/bcdd/BCalcWrapper.py45
-rw-r--r--src/bcdd/DDTable.py98
-rw-r--r--src/bcdd/Exceptions.py18
-rw-r--r--src/bcdd/PBNBoard.py242
-rw-r--r--src/bcdd/PBNFile.py43
-rw-r--r--src/bcdd/ParContract.py155
-rw-r--r--src/bcdd/ParScore.py171
-rw-r--r--src/bcdd/__init__.py0
-rw-r--r--src/jfrteamy/__init__.py0
-rw-r--r--src/jfrteamy/db.py47
-rw-r--r--src/main.py182
11 files changed, 1001 insertions, 0 deletions
diff --git a/src/bcdd/BCalcWrapper.py b/src/bcdd/BCalcWrapper.py
new file mode 100644
index 0000000..a6bed27
--- /dev/null
+++ b/src/bcdd/BCalcWrapper.py
@@ -0,0 +1,45 @@
+'''
+Wrapper class for libbcalcDDS.dll
+'''
+
+import os
+from ctypes import cdll, c_char_p, c_void_p
+
+from .Exceptions import DllNotFoundException
+
+
+class BCalcWrapper(object):
+
+ DENOMINATIONS = [ 'C', 'D', 'H', 'S', 'N' ]
+ PLAYERS = [ 'N', 'E', 'S', 'W' ]
+
+ def __init__(self):
+ dllPath = os.path.join(
+ os.path.dirname(__file__),
+ '..')
+ try:
+ self.libbcalcdds = cdll.LoadLibrary(os.path.join(
+ dllPath, 'libbcalcdds.dll'))
+ except OSError:
+ try:
+ self.libbcalcdds = cdll.LoadLibrary(os.path.join(
+ dllPath, 'libbcalcdds.so'))
+ except OSError:
+ self.libbcalcdds = None
+ if self.libbcalcdds is None:
+ raise DllNotFoundException()
+
+ def __getattr__(self, attrname):
+ def _dynamic_method(*args):
+ function = getattr(self.libbcalcdds, 'bcalcDDS_' + attrname)
+ if attrname == 'new':
+ function.restype = c_void_p
+ else:
+ if attrname == 'getLastError':
+ function.restype = c_char_p
+ function.argtypes = [c_void_p]
+ return function(*args)
+ return _dynamic_method
+
+ def declarerToLeader(self, player):
+ return (player + 1) % 4
diff --git a/src/bcdd/DDTable.py b/src/bcdd/DDTable.py
new file mode 100644
index 0000000..651aec3
--- /dev/null
+++ b/src/bcdd/DDTable.py
@@ -0,0 +1,98 @@
+from ctypes import c_char_p
+from copy import copy
+
+from .BCalcWrapper import BCalcWrapper
+from .Exceptions import DDTableInvalidException, FieldNotFoundException
+
+
+class DDTable(object):
+
+ def _get_empty_table(self):
+ table = []
+ row = [-1] * 5
+ for col in range(0, 4):
+ table.append(copy(row))
+ return table
+
+ def _validate_table(self, table):
+ for row in table:
+ for t in row:
+ if (t > 13) or (t < 0):
+ raise DDTableInvalidException(
+ 'Invalid number of tricks: %d' % (t)
+ )
+ return table
+
+ _banner_displayed = False
+
+ def __init__(self, board):
+ self._board = board;
+ self._wrapper = BCalcWrapper()
+
+ def _check_for_error(self, solver):
+ error = self._wrapper.getLastError(solver)
+ if error:
+ raise DDTableInvalidException(
+ 'BCalc error: %s' % (c_char_p(error).value.decode('ascii')))
+
+ def get_bcalc_table(self, show_banner=True):
+ if not DDTable._banner_displayed and show_banner:
+ print('Double dummy analysis provided by BCalc.')
+ print('BCalc is awesome, check it out: http://bcalc.w8.pl')
+ DDTable._banner_displayed = True
+ result = self._get_empty_table()
+ deal = self._board.get_layout()
+ solver = self._wrapper.new(b"PBN", deal.encode(), 0, 0)
+ self._check_for_error(solver)
+ for denom in range(0, 5):
+ self._wrapper.setTrumpAndReset(solver, denom)
+ for player in range(0, 4):
+ leader = self._wrapper.declarerToLeader(player)
+ self._wrapper.setPlayerOnLeadAndReset(solver, leader)
+ result[player][denom] = 13 - self._wrapper.getTricksToTake(
+ solver)
+ self._check_for_error(solver)
+ self._wrapper.delete(solver);
+ return self._validate_table(result)
+
+ def get_jfr_table(self):
+ result = self._get_empty_table()
+ ability = self._board.get_ability()
+ abilities = self._board.validate_ability(ability)
+ for player_ability in abilities:
+ player = player_ability[0]
+ player_id = BCalcWrapper.PLAYERS.index(player)
+ denom_id = 4
+ for tricks in player_ability[1]:
+ result[player_id][denom_id] = int(tricks, 16)
+ denom_id -= 1
+ return self._validate_table(result)
+
+ def get_pbn_table(self):
+ table = self._board.get_optimum_result_table()
+ parsed_table = self._board.validate_optimum_result_table(table)
+ result = self._get_empty_table()
+ for line_match in parsed_table:
+ player = line_match.group(1)[0]
+ denom = line_match.group(2)[0]
+ tricks = int(line_match.group(3))
+ player_id = BCalcWrapper.PLAYERS.index(player)
+ denom_id = BCalcWrapper.DENOMINATIONS.index(denom)
+ result[player_id][denom_id] = tricks
+ return self._validate_table(result)
+
+ def get_dd_table(self, show_banner=True):
+ try:
+ return self.get_jfr_table()
+ except FieldNotFoundException:
+ try:
+ return self.get_pbn_table()
+ except FieldNotFoundException:
+ return self.get_bcalc_table(show_banner)
+
+ def print_table(self, dd_table):
+ print('\t' + '\t'.join(BCalcWrapper.DENOMINATIONS))
+ for i in range(0, 4):
+ print('%s%s' % (
+ self._wrapper.PLAYERS[i],
+ ''.join(['\t' + str(tricks) for tricks in dd_table[i]])))
diff --git a/src/bcdd/Exceptions.py b/src/bcdd/Exceptions.py
new file mode 100644
index 0000000..fd81dd7
--- /dev/null
+++ b/src/bcdd/Exceptions.py
@@ -0,0 +1,18 @@
+class FileNotFoundError(Exception):
+ pass
+
+
+class DllNotFoundException(Exception):
+ pass
+
+
+class FieldNotFoundException(Exception):
+ pass
+
+
+class ParScoreInvalidException(FieldNotFoundException):
+ pass
+
+
+class DDTableInvalidException(FieldNotFoundException):
+ pass
diff --git a/src/bcdd/PBNBoard.py b/src/bcdd/PBNBoard.py
new file mode 100644
index 0000000..87105f2
--- /dev/null
+++ b/src/bcdd/PBNBoard.py
@@ -0,0 +1,242 @@
+import re
+
+from .BCalcWrapper import BCalcWrapper
+from .Exceptions import FieldNotFoundException, DDTableInvalidException
+
+
+class PBNField(object):
+
+ def __init__(self, key=None, value=None, raw_data=None):
+ self.key = key
+ self.value = value
+ self.raw_field = '[%s "%s"]' % (self.key, str(self.value)) \
+ if self.key is not None else raw_data
+
+class PBNBoard(object):
+
+ line_pattern = re.compile(r'\[(.*) "(.*)"\]')
+ ability_pattern = re.compile(r'\b([NESW]):([0-9A-D]{5})\b')
+ optimum_result_table_pattern = re.compile(
+ r'^([NESW])\s+([CDHSN])T?\s+(\d+)$')
+
+ def __init__(self, lines, line_no=None):
+ self.line_number = line_no
+ self._has_optimum_result_table = None
+ self._has_ability = None
+ self.fields = []
+ for line in lines:
+ field = PBNField(raw_data=line)
+ line_parse = self.line_pattern.match(line)
+ if line_parse:
+ field.key = line_parse.group(1)
+ field.value = line_parse.group(2)
+ self.fields.append(field)
+
+ def has_field(self, key):
+ for field in self.fields:
+ if key == field.key:
+ return True
+ return False
+
+ def get_field(self, key):
+ for field in self.fields:
+ if key == field.key:
+ return field.value
+ raise FieldNotFoundException(key + ' field not found')
+
+ def delete_field(self, key):
+ to_remove = []
+ for field in self.fields:
+ if key == field.key:
+ to_remove.append(field)
+ for remove in to_remove:
+ self.fields.remove(remove)
+
+ def get_event(self):
+ return self.get_field('Event')
+
+ def write_event(self, name):
+ for i in range(0, len(self.fields)):
+ if 'Board' == self.fields[i].key:
+ self.fields.insert(i, PBNField(key='Event', value=name))
+ break
+
+ def get_layout(self):
+ return self.get_field('Deal')
+
+ def get_number(self):
+ return self.get_field('Board')
+
+ def get_vulnerable(self):
+ return self.get_field('Vulnerable')
+
+ def get_dealer(self):
+ return self.get_field('Dealer')
+
+ def validate_ability(self, ability):
+ matches = self.ability_pattern.findall(ability)
+ if not len(matches):
+ self._has_ability = False
+ raise DDTableInvalidException('Invalid Ability line: ' + ability)
+ players = []
+ for match in matches:
+ if match[0] in players:
+ self._has_ability = False
+ raise DDTableInvalidException(
+ 'Duplicate entry in Ability: ' + match[0])
+ else:
+ players.append(match[1])
+ self._has_ability = False
+ return matches
+
+ def get_ability(self):
+ return self.get_field('Ability')
+
+ def delete_ability(self):
+ self.delete_field('Ability')
+
+ def write_ability(self, dd_table):
+ sb = ''
+ for i in range(0, 4):
+ sb += BCalcWrapper.PLAYERS[i]
+ sb += ':'
+ sb += ''.join(['%X' % (j) for j in dd_table[i]])[::-1]
+ sb += ' '
+ self.fields.append(PBNField(key='Ability', value=sb.strip()))
+
+ def get_minimax(self):
+ return self.get_field('Minimax')
+
+ def delete_minimax(self):
+ self.delete_field('Minimax')
+
+ def write_minimax(self, contract):
+ minimax = '7NS0' if contract.score == 0 \
+ else '%d%s%s%s%d' % (
+ contract.level,
+ contract.denomination,
+ 'D' if contract.doubled else '',
+ contract.declarer,
+ contract.score)
+ self.fields.append(PBNField(key='Minimax', value=minimax))
+
+ def get_optimum_score(self):
+ return self.get_field('OptimumScore')
+
+ def delete_optimum_score(self):
+ self.delete_field('OptimumScore')
+
+ def write_optimum_score(self, contract):
+ self.fields.append(
+ PBNField(key='OptimumScore',
+ value='NS %d' % (contract.score)))
+
+ def get_optimum_result(self):
+ return self.get_field('OptimumResult')
+
+ def validate_optimum_result_table(self, table):
+ matches = []
+ duplicates = []
+ for line in table:
+ match = self.optimum_result_table_pattern.match(line)
+ if not match:
+ self._has_optimum_result_table = False
+ raise DDTableInvalidException(
+ 'Invalid OptimumResultTable line: ' + line)
+ position = match.group(1) + ' - ' + match.group(2)
+ if position in duplicates:
+ self._has_optimum_result_table = False
+ raise DDTableInvalidException(
+ 'Duplicate OptimumResultTable line: ' + line)
+ else:
+ duplicates.append(position)
+ matches.append(match)
+ self._has_optimum_result_table = True
+ return matches
+
+ def get_optimum_result_table(self):
+ try:
+ return self.get_extended_raw_field('OptimumResultTable')
+ except FieldNotFoundException:
+ self._has_optimum_result_table = False
+ raise
+
+ def get_auction(self):
+ return self.get_extended_raw_field('Auction', with_header=True)
+
+ def get_extended_raw_field(self, field_name, with_header=False):
+ field_found = False
+ result = []
+ for field in self.fields:
+ if field_name == field.key:
+ field_found = True
+ if with_header:
+ result.append(field.value)
+ else:
+ if field_found:
+ if field.key is None:
+ result.append(field.raw_field)
+ else:
+ break
+ if not field_found:
+ raise FieldNotFoundException('%s field not found' % (field_name))
+ return result
+
+ def delete_optimum_result_table(self):
+ field_found = False
+ to_remove = []
+ for field in self.fields:
+ if 'OptimumResultTable' == field.key:
+ field_found = True
+ to_remove.append(field)
+ else:
+ if field_found:
+ if field.key is None:
+ to_remove.append(field)
+ else:
+ break
+ for remove in to_remove:
+ self.fields.remove(remove)
+
+ def write_optimum_result_table(self, dd_table):
+ self.fields.append(PBNField(
+ key='OptimumResultTable',
+ value=r'Declarer;Denomination\2R;Result\2R'))
+ for i in range(0, 4):
+ for j in range(0, 5):
+ self.fields.append(PBNField(
+ raw_data='%s %s%s %d' % (
+ BCalcWrapper.PLAYERS[i],
+ BCalcWrapper.DENOMINATIONS[j],
+ 'T' if BCalcWrapper.DENOMINATIONS[j] == 'N' else '',
+ dd_table[i][j])))
+
+ def save_par_contract(self, contract, jfr_only=False):
+ if not jfr_only:
+ self.delete_optimum_score()
+ self.write_optimum_score(contract)
+ self.delete_minimax()
+ self.write_minimax(contract)
+
+ def save_dd_table(self, dd_table, jfr_only=False):
+ if not jfr_only:
+ if self._has_optimum_result_table is None:
+ try:
+ optimum_result_table = self.validate_optimum_result_table(
+ self.get_optimum_result_table())
+ self._has_optimum_result_table = True
+ except FieldNotFoundException:
+ self._has_optimum_result_table = False
+ if not self._has_optimum_result_table:
+ self.delete_optimum_result_table()
+ self.write_optimum_result_table(dd_table)
+ if self._has_ability is None:
+ try:
+ ability = self.validate_ability(
+ self.get_ability())
+ self._has_ability = True
+ except FieldNotFoundException:
+ self._has_ability = False
+ if not self._has_ability:
+ self.delete_ability()
+ self.write_ability(dd_table)
diff --git a/src/bcdd/PBNFile.py b/src/bcdd/PBNFile.py
new file mode 100644
index 0000000..f5436ad
--- /dev/null
+++ b/src/bcdd/PBNFile.py
@@ -0,0 +1,43 @@
+import shutil
+import tempfile
+
+from .PBNBoard import PBNBoard
+
+class PBNFile(object):
+
+ def __init__(self, filename):
+ self._filename = filename
+ self.output_file = None
+ self.boards = []
+ lines = []
+ with open(self._filename) as pbn_file:
+ contents = pbn_file.readlines()
+ first_line = 1
+ for line_no in range(0, len(contents)):
+ line = contents[line_no].strip()
+ if not line:
+ if len(lines) > 0:
+ self.boards.append(PBNBoard(lines, first_line))
+ lines = []
+ first_line = line_no + 2
+ else:
+ lines.append(line)
+ if len(lines) > 0:
+ self.boards.append(PBNBoard(lines, first_line))
+ if not self.boards[0].has_field('Event'):
+ self.boards[0].write_event('')
+
+ def write_board(self, board):
+ if self.output_file is None:
+ self.output_file = tempfile.NamedTemporaryFile(
+ mode='w', delete=False)
+ for field in board.fields:
+ self.output_file.write(field.raw_field + '\r\n')
+ self.output_file.write('\r\n')
+
+ def save(self):
+ if self.output_file is None:
+ raise IOError('No boards written to PBN file, unable to save it.')
+ tmp_path = self.output_file.name
+ self.output_file.close()
+ shutil.move(tmp_path, self._filename)
diff --git a/src/bcdd/ParContract.py b/src/bcdd/ParContract.py
new file mode 100644
index 0000000..918efae
--- /dev/null
+++ b/src/bcdd/ParContract.py
@@ -0,0 +1,155 @@
+import functools
+
+from .BCalcWrapper import BCalcWrapper as bcw
+from .Exceptions import ParScoreInvalidException
+
+
+class ParContract(object):
+ def __init__(self, level=0, denom='', declarer='', doubled=False, score=0):
+ self.level = level
+ self.denomination = denom
+ self.declarer = declarer
+ self.doubled = doubled
+ self.score = score
+
+ def validate(self):
+ if self.score == 0:
+ return self
+ if (self.level < 1) or (self.level > 7):
+ raise ParScoreInvalidException(
+ 'Invalid par contract level: %d' % (self.level))
+ if self.denomination not in 'CDHSN':
+ raise ParScoreInvalidException(
+ 'Invalid par contract denomination: ' + self.denomination)
+ if self.declarer not in 'NESW':
+ raise ParScoreInvalidException(
+ 'Invalid par contract declarer: ' + self.declarer)
+ return self
+
+ def __repr__(self):
+ if self.score == 0:
+ return 'PASS'
+ return '%d%s%s %s %+d' % (
+ self.level,
+ self.denomination,
+ 'x' if self.doubled else '',
+ self.declarer,
+ self.score)
+
+ def __eq__(self, other):
+ return hash(self) == hash(other)
+
+ def __hash__(self):
+ return self.score + self.level + 10000 * (
+ ord(self.denomination[0]) if self.denomination else 0)
+
+ def calculate_score(self, tricks, vulnerable=False):
+ if self.level == 0:
+ return 0
+ score = 0
+ if self.level + 6 > tricks:
+ undertricks = self.level + 6 - tricks
+ if self.doubled:
+ while True:
+ if undertricks == 1:
+ # first undertrick: 100 non-vul, 200 vul
+ score -= 200 if vulnerable else 100
+ else:
+ if (undertricks <= 3) and not vulnerable:
+ # second non-vul undertrick: 200
+ score -= 200
+ else:
+ # further undertricks: 300
+ score -= 300
+ undertricks -= 1
+ if undertricks == 0:
+ break;
+ else:
+ score = -100 if vulnerable else -50
+ score *= undertricks
+ else:
+ par_tricks = self.level
+ while True:
+ if (self.denomination == 'N') and (par_tricks == 1):
+ # first non-trump trick: 40
+ score += 40
+ else:
+ # other tricks
+ score += 30 if self.denomination in 'NSH' else 20
+ par_tricks -= 1
+ if par_tricks == 0:
+ break
+ overtricks = tricks - self.level - 6
+ if self.doubled:
+ score *= 2
+ score += 50
+ # game premium
+ score += (500 if vulnerable else 300) if (score >= 100) else 50
+ if self.doubled:
+ score += overtricks * (200 if vulnerable else 100)
+ else:
+ score += overtricks * (20 if self.denomination in 'CD' else 30)
+ if self.level == 7:
+ # grand slam premium
+ score += 1500 if vulnerable else 1000
+ elif self.level == 6:
+ # small slam premium
+ score += 750 if vulnerable else 500
+ if self.declarer in 'EW':
+ score = -score
+ return score
+
+ def __gt__(self, other):
+ denomination = bcw.DENOMINATIONS.index(self.denomination) \
+ if self.denomination in bcw.DENOMINATIONS \
+ else -1
+ other_denomination = bcw.DENOMINATIONS.index(
+ other.denomination) \
+ if other.denomination in bcw.DENOMINATIONS else -1
+ return (self.level > other.level) \
+ or ((self.level == other.level) \
+ and (denomination > other_denomination))
+
+ def get_defense(self, dd_table, vulnerable):
+ declarer_index = bcw.PLAYERS.index(self.declarer) \
+ if self.declarer in bcw.PLAYERS else -1
+ denomination_index = bcw.DENOMINATIONS.index(self.denomination) \
+ if self.denomination in bcw.DENOMINATIONS else -1
+ if (self.level != 0) \
+ and (self.level + 6
+ <= dd_table[declarer_index][denomination_index]):
+ defenders_indexes = []
+ defenders_indexes.append((declarer_index + 1) % 4);
+ defenders_indexes.append((declarer_index + 3) % 4);
+ possible_defense = []
+ score_squared = self.score * self.score
+ for i in range(0, 5):
+ level = self.level
+ if i <= denomination_index:
+ level += 1
+ if level <= 7:
+ for defender in defenders_indexes:
+ if level + 6 > dd_table[defender][i]:
+ defense = ParContract(
+ level,
+ bcw.DENOMINATIONS[i],
+ bcw.PLAYERS[defender],
+ True,
+ 0)
+ defense.score = defense.calculate_score(
+ dd_table[defender][i],
+ vulnerable)
+ if score_squared > self.score * defense.score:
+ possible_defense.append(defense)
+ if possible_defense:
+ possible_defense.sort(
+ key=lambda x: abs(x.score - self.score))
+ optimum_defense = possible_defense[-1]
+ possible_defense = [defense for defense in possible_defense
+ if defense.score == optimum_defense.score]
+ for defense in possible_defense:
+ # Lowest from the most profitable sacrifices
+ if optimum_defense > defense:
+ optimum_defense = defense
+ return optimum_defense
+ return None
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)
diff --git a/src/bcdd/__init__.py b/src/bcdd/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/bcdd/__init__.py
diff --git a/src/jfrteamy/__init__.py b/src/jfrteamy/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/jfrteamy/__init__.py
diff --git a/src/jfrteamy/db.py b/src/jfrteamy/db.py
new file mode 100644
index 0000000..b66f8d0
--- /dev/null
+++ b/src/jfrteamy/db.py
@@ -0,0 +1,47 @@
+import sys
+
+
+class TeamyDB(object):
+
+ db_cursor = None
+
+ def __init__(self, settings):
+ reload(sys)
+ sys.setdefaultencoding("latin1")
+ import mysql.connector
+ self.database = mysql.connector.connect(
+ user=settings['user'],
+ password=settings['password'],
+ host=settings['host'],
+ port=settings.get('port', 3306),
+ database=settings['database'])
+ self.db_cursor = self.database.cursor(buffered=True)
+ self.db_name = settings['database']
+
+ def get_cursor(self):
+ return self.db_cursor
+
+ def __execute_query(self, sql, params):
+ #print('DEBUG: query (%s): %s | %s' % (self.db_name, sql.replace('\n', ' '), params))
+ self.db_cursor.execute(sql, params)
+
+ def fetch(self, sql, params=None):
+ import mysql.connector
+ try:
+ self.__execute_query(sql, params)
+ row = self.db_cursor.fetchone()
+ return row
+ except mysql.connector.Error as e:
+ print('ERROR: ' + str(e))
+ raise IOError(e.errno, str(e), sql)
+
+ def fetch_all(self, sql, params=None):
+ import mysql.connector
+ try:
+ self.__execute_query(sql, params)
+ results = self.db_cursor.fetchall()
+ return results
+ except mysql.connector.Error as e:
+ print('ERROR: ' + str(e))
+ raise IOError(
+ e.errno, str(e), sql)
diff --git a/src/main.py b/src/main.py
new file mode 100644
index 0000000..f0457ca
--- /dev/null
+++ b/src/main.py
@@ -0,0 +1,182 @@
+import json, os, sys
+
+from bcdd.PBNFile import PBNFile
+from jfrteamy.db import TeamyDB
+
+
+def get_digits(t):
+ return int(''.join(i for i in t if i.isdigit()))
+
+
+def clear():
+ os.system('cls' if os.name == 'nt' else 'clear')
+
+
+with open(sys.argv[1]) as config_file:
+ config = json.load(config_file)
+
+f = PBNFile('../Segment.pbn')
+db = TeamyDB(config['mysql'])
+
+clear()
+
+if config['settings']['fetch_lineups'] > 0:
+
+ players = db.fetch_all('SELECT id, team, CONCAT(gname, " ", sname) FROM players')
+ rosters = {}
+ for player in players:
+ team = player[1]
+ if team not in rosters:
+ rosters[team] = {}
+ rosters[team][player[0]] = player[2]
+
+ tables = {}
+ for b in f.boards:
+ if b.has_field('Round'):
+ if b.get_field('Round') == str(config['settings']['pbn_round']):
+ table = b.get_field('Table')
+ if table not in tables:
+ tables[table] = {}
+ tables[table][b.get_field('Room')] = [
+ [b.get_field('North'), b.get_field('South')],
+ [b.get_field('East'), b.get_field('West')]
+ ]
+
+ db_tables = db.fetch_all('SELECT tabl, homet, visit FROM segments WHERE rnd = %s AND segment = %s', (
+ config['settings']['teamy_round'],
+ config['settings']['teamy_segment']
+ ))
+ round_lineup = {}
+ for dbt in db_tables:
+ round_lineup[dbt[0]] = dbt[1:]
+
+ for t, rooms in tables.items():
+ table = get_digits(t)
+ home_team = round_lineup[table][0]
+ away_team = round_lineup[table][1]
+ home_roster = rosters[home_team]
+ away_roster = rosters[away_team]
+ lineups = {}
+ positions = [['N', 'S'], ['E', 'W']]
+ for room, lineup in rooms.items():
+ room = room.lower().replace('closed', 'close')
+ for which_room in ['open', 'close']:
+ roster = home_roster if which_room == 'open' else away_roster
+ team = home_team if which_room == 'open' else away_team
+ for i in range(0, 2):
+ player = lineup[1 - (room == which_room)][i]
+ player_id = None
+ for roster_id, roster_pl in roster.items():
+ if player == roster_pl:
+ player_id = roster_id
+ position = room + positions[1 - (room == which_room)][i]
+ print('INFO: Player in lineup: Table %d, position %s, #%d %s' % (
+ table, position,
+ roster_id, roster_pl))
+ db.fetch(
+ 'UPDATE segments SET '+position+' = %s WHERE rnd = %s AND segment = %s AND tabl = %s', (
+ player_id,
+ config['settings']['teamy_round'], config['settings']['teamy_segment'],
+ table
+ )
+ )
+ if player_id is None:
+ print('WARNING: Player %s not found in team %d', (player, team))
+
+board_mapping = {}
+for b in db.fetch_all('SELECT brd, bno FROM boards WHERE rnd = %s AND segment = %s', (
+ config['settings']['teamy_round'], config['settings']['teamy_segment'])):
+ board_mapping[b[1]] = b[0]
+
+for b in f.boards:
+ if b.has_field('Round'):
+ if b.get_field('Round') == str(config['settings']['pbn_round']):
+ board = int(b.get_field('Board'))
+ if board not in board_mapping:
+ print('ERROR: board %d not meant to be played in segment %d-%d' % (
+ board, config['settings']['teamy_round'], config['settings']['teamy_segment']))
+ continue
+ board_no = board_mapping[board]
+ table = get_digits(b.get_field('Table'))
+ room = 1 if b.get_field('Room') == 'Open' else 2
+ while True:
+ current_score = db.fetch('SELECT declarer, contract, result, lead, score FROM scores ' +
+ 'WHERE rnd = %s AND segment = %s AND tabl = %s AND room = %s AND board = %s', (
+ config['settings']['teamy_round'], config['settings']['teamy_segment'],
+ table, room, board_no))
+ if current_score:
+ break
+ print('INFO: record in scores table does not exist - creating')
+ db.fetch('INSERT INTO scores(rnd, segment, tabl, room, board, mecz, butler, processed, tims) VALUES(%s, %s, %s, %s, %s, 0, 0, 1, NOW())', (
+ config['settings']['teamy_round'], config['settings']['teamy_segment'],
+ table, room, board_no))
+
+ declarer = b.get_field('Declarer')
+ contract = b.get_field('Contract').replace('*', ' x').replace('x x', 'xx')
+ if contract[0].isdigit():
+ contract = contract[0] + ' ' + contract[1:]
+ result = int(b.get_field('Result')) - get_digits(contract) - 6
+ lead = '' # wtf?
+ score = int(b.get_field('Score').replace('NS ', '')) # co z pasami?
+
+ update_score = True
+ if current_score[4] is not None:
+ if not config['settings']['overwrite_scores']:
+ update_score = False
+ if score != current_score[4]:
+ print('WARNING: result in board %d, table %d-%d changed and is not going to be overwritten!' % (
+ board, table, room))
+ else:
+ print('INFO: not overwriting result in board %d, table %d-%d' % (
+ board, table, room))
+ if update_score:
+ params = (contract, declarer, result, lead, score)
+ print('INFO: updating result in board %d, table %d-%d: %s' % (
+ board, table, room, params))
+ db.fetch('UPDATE scores SET contract = %s, declarer = %s, result = %s, lead = %s, score = %s, '+
+ 'tims = NOW(), processed = 0, mecz = 1, butler = 1 '+
+ 'WHERE rnd = %s AND segment = %s AND board = %s AND tabl = %s AND room = %s', (
+ params + (config['settings']['teamy_round'], config['settings']['teamy_segment'], board_no, table, room)))
+
+ if not b.has_field('Auction'):
+ print('INFO: no bidding for board %d, table %d-%d' % (
+ board, table, room))
+ continue
+ bidding = b.get_auction()
+ dealer = bidding[0]
+ if dealer != b.get_field('Dealer'):
+ print('WARNING: bidding does not start from the dealer in board %d, table %d-%d' % (
+ board, table, room))
+ bidding = ' '.join(bidding[1:]).split(' ')
+ html_bidding = []
+ lin_bidding = []
+ for bid in bidding:
+ if bid == 'Pass':
+ lin_bidding.append('p')
+ html_bidding.append('pass')
+ continue
+ bid = bid.replace('NT', 'N')
+ html_bid = bid
+ for suit in ['C', 'D', 'H', 'S', 'N']:
+ html_bid = html_bid.replace(suit, "<img src='images/%s.gif'>" % (suit))
+ html_bidding.append(html_bid)
+ lin_bidding.append(bid)
+ lin_bidding = 'mb|'.join([bid + '|' for bid in lin_bidding]) + 'pg||'
+ players = ['W', 'N', 'E', 'S']
+ html_bidding = ['&nbsp;'] * players.index(dealer) + html_bidding + ['&nbsp;'] * ((4 - players.index(dealer)) % 4)
+ html = "<table border=0><tr><td align='center'>&nbsp;&nbsp;&nbsp;W&nbsp;&nbsp;&nbsp;</td><td align='center'>&nbsp;&nbsp;&nbsp;N&nbsp;&nbsp;&nbsp;</td><td align='center'>&nbsp;&nbsp;&nbsp;E&nbsp;&nbsp;&nbsp;</td><td align='center'>&nbsp;&nbsp;&nbsp;S&nbsp;&nbsp;&nbsp;</td></tr>"
+ for i in range(0, len(html_bidding)):
+ if i % 4 == 0:
+ html += '<tr>'
+ html += "<td align='center'>"
+ html += html_bidding[i]
+ html += "</td>"
+ if i % 4 == 3:
+ html += "</tr>"
+ html += '</table>'
+ print('INFO: updating bidding in board %d, table %d-%d' % (
+ board, table, room))
+ db.fetch('UPDATE scores SET auction = %s, bbo = %s, '+
+ 'tims = NOW(), processed = 0 '+
+ 'WHERE rnd = %s AND segment = %s AND board = %s AND tabl = %s AND room = %s', (
+ html, lin_bidding, config['settings']['teamy_round'], config['settings']['teamy_segment'], board_no, table, room))