Source code for results.views.core

from types import SimpleNamespace

from results.models import PlayerSummaryResult, ResultsFile

try:
    from ddstable import ddstable
except OSError as err:
    print(err)
    print(
        "Error loading ddstable. Most likely cause is wrong version or missing libdds.so"
    )
    print("Double dummy analysis will fail affecting Results")


[docs] def higher_than_other_suit(suit, other_suit): """Checks if a suit is higher than another in bridge terms""" all_suits = "CDHSN" suit_loc = all_suits.find(suit) other_suit_loc = all_suits.find(other_suit) return suit_loc > other_suit_loc
[docs] def lower_than_other_contract(contract, other_contract): """Checks if a contract is lower than another in bridge terms""" all_suits = "CDHSN" level = int(contract[0]) suit = contract[1] level_other_contract = int(other_contract[0]) suit_other_contract = other_contract[1] if level < level_other_contract: return True if level > level_other_contract: return False # Get suit order - levels are the same suit_loc = all_suits.find(suit) other_suit_loc = all_suits.find(suit_other_contract) return suit_loc < other_suit_loc
[docs] def next_contract_up(contract): """return next contract or False. 4N -> 5C, 1S -> 1N, 7N -> False. Ignores X or XX """ suits = "CDHSN" level = int(contract[0]) suit = contract[1] loc = suits.find(suit) if loc == 4: level += 1 suit = "C" else: suit = suits[loc + 1] return f"{level}{suit}"
[docs] def partner_for(player): """return player's partner""" partners = {"N": "S", "S": "N", "E": "W", "W": "E"} return partners[player]
[docs] def opponent_for(player): """return player's opponent (any)""" return "E" if player in ["N", "S"] else "N"
[docs] def opponents_list_for(player): """return player's opponent as a list""" return ["E", "W"] if player in ["N", "S"] else ["N", "S"]
[docs] def get_recent_results(user): """Return the 5 most recent results for a user. Called by Dashboard""" results = PlayerSummaryResult.objects.filter( player_system_number=user.system_number, results_file__status=ResultsFile.ResultsStatus.PUBLISHED, ).order_by("-result_date")[:6] # We get 6, so we can return 5 but show if more are available if len(results) == 6: return results[:5], True else: return results, False
[docs] def double_dummy_from_usebio(board): """perform a double dummy analysis of a hand. This requires libdds.so to be available on the path. See the documentation if you need to rebuild this for your OS. It works fine on Linux out of the box. Nothing extra required, just pip install ddstable. We expect to get part of the USEBIO XML - ['USEBIO']["HANDSET"]["BOARD"][?] Data contains a list with direction and suits ('DIRECTION', 'East'), ('CLUBS', 'Q6542'), ('DIAMONDS', '85'),... """ # Build PBN format string hand = { compass["DIRECTION"]: { "clubs": compass["CLUBS"], "diamonds": compass["DIAMONDS"], "hearts": compass["HEARTS"], "spades": compass["SPADES"], } for compass in board } pbn_str = f"N:{hand['North']['spades']}.{hand['North']['hearts']}.{hand['North']['diamonds']}.{hand['North']['clubs']}" pbn_str += f" {hand['East']['spades']}.{hand['East']['hearts']}.{hand['East']['diamonds']}.{hand['East']['clubs']}" pbn_str += f" {hand['South']['spades']}.{hand['South']['hearts']}.{hand['South']['diamonds']}.{hand['South']['clubs']}" pbn_str += f" {hand['West']['spades']}.{hand['West']['hearts']}.{hand['West']['diamonds']}.{hand['West']['clubs']}" pbn_bytes = bytes(pbn_str, encoding="utf-8") return ddstable.get_ddstable(pbn_bytes)
def _score_for_contract_static(contract, vulnerability, declarer): """set up static data for this session""" # Breakdown the contract tricks_committed = int(contract[0]) + 6 denomination = contract[1] if contract[2:] == "XX": redoubled = True doubled = False factor = 4 elif contract[2:] == "X": redoubled = False doubled = True factor = 2 else: redoubled = False doubled = False factor = 0 # Set vulnerable is_vulnerable = bool(declarer in ["N", "S"] and vulnerability in ["NS", "All"]) or ( declarer in ["E", "W"] and vulnerability in ["EW", "All"] ) # use part score bonus (50) as starting point starting_making_score = 50 # For NT add on the 10 for the first trick if denomination == "N": starting_making_score += 10 # Set bonuses and over tricks if is_vulnerable: # Vul game_bonus = 450 # 500, but we already gave 50 slam_bonus = 1200 # 750 + 500 for game but we already gave 50 grand_slam_bonus = 1950 # 1500 + 500 for game but we already gave 50 doubled_over_trick_value = 200 redoubled_over_trick_value = 400 under_trick_value = 100 else: # Non-Vul game_bonus = 250 # 300, but we already gave 50 slam_bonus = 750 # 500 + 300 for game, but we already gave 50 grand_slam_bonus = 1250 # 1000 + 300 for game, but we already gave 50 doubled_over_trick_value = 100 redoubled_over_trick_value = 200 under_trick_value = 50 score_per_trick = 20 if denomination in ["C", "D"] else 30 data = { "starting_making_score": starting_making_score, "game_bonus": game_bonus, "slam_bonus": slam_bonus, "grand_slam_bonus": grand_slam_bonus, "is_vulnerable": is_vulnerable, "tricks_committed": tricks_committed, "denomination": denomination, "doubled": doubled, "redoubled": redoubled, "score_per_trick": score_per_trick, "doubled_over_trick_value": doubled_over_trick_value, "redoubled_over_trick_value": redoubled_over_trick_value, "under_trick_value": under_trick_value, "factor": factor, "declarer": declarer, } # return as a dotable thing. Pretty cool. return SimpleNamespace(**data) def _score_for_contract_add_bonus(static, tricks_taken, score): """sub to calculate bonuses For doubled contracts we will get the tricks_taken and tricks_committed as doubled values which could be greater than 13 """ # You can be doubled into game (but not into a slam) so use different counters for double/redoubled scores if static.doubled: game_tricks_committed = 6 + (static.tricks_committed - 6) * 2 elif static.redoubled: game_tricks_committed = 6 + (static.tricks_committed - 6) * 4 else: game_tricks_committed = static.tricks_committed # Grand slam if tricks_taken >= 13 and static.tricks_committed >= 13: score += static.grand_slam_bonus # small slam elif tricks_taken >= 12 and static.tricks_committed >= 12: score += static.slam_bonus # game elif ( (static.denomination in ["C", "D"] and game_tricks_committed >= 11) or (static.denomination in ["H", "S"] and game_tricks_committed >= 10) or (static.denomination == "N" and game_tricks_committed >= 9) ): score += static.game_bonus return score def _score_for_contract_making(static, tricks_taken): """sub for making contracts""" # handle doubled and redoubled making if static.doubled or static.redoubled: if static.doubled: over_trick_value = static.doubled_over_trick_value insult = 50 else: # redoubled over_trick_value = static.redoubled_over_trick_value insult = 100 # score for bonus calculations excludes over tricks, we add them later over_tricks = tricks_taken - static.tricks_committed score = ( (static.tricks_committed - 6) * static.score_per_trick * static.factor ) + static.starting_making_score # for NT we need to adjust if static.denomination == "N": score = ( ((static.tricks_committed - 6) * static.score_per_trick + 10) * static.factor ) + 50 else: # not doubled # starting score = score for tricks + base score (part score) score = ( (tricks_taken - 6) * static.score_per_trick ) + static.starting_making_score # add bonuses if appropriate score = _score_for_contract_add_bonus(static, tricks_taken, score) # handle over tricks and insult if static.doubled or static.redoubled: # add to score and add the insult score += (over_tricks * over_trick_value) + insult # scores are always with reference to NS if static.declarer in ["E", "W"]: score = -score return score def _score_for_contract_failing(static, tricks_taken): """sub for contracts going off""" under_tricks = static.tricks_committed - tricks_taken if static.doubled or static.redoubled: # fmt: off if static.is_vulnerable and static.doubled: penalties = [200, 300, 300, 300, 300, 300, 300, 300, 300, 300, 300, 300, 300] if static.is_vulnerable and static.redoubled: penalties = [400, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600] if not static.is_vulnerable and static.doubled: penalties = [100, 200, 200, 300, 300, 300, 300, 300, 300, 300, 300, 300, 300] if not static.is_vulnerable and static.redoubled: penalties = [200, 400, 400, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600] # fmt: on score = 0 index = 0 while under_tricks > 0: score += penalties[index] index += 1 under_tricks -= 1 else: # not doubled score = static.under_trick_value * under_tricks # scores are always with reference to NS if static.declarer in ["E", "W"]: score = -score return -score
[docs] def score_for_contract(contract, vulnerability, declarer, tricks_taken): """Calculate the score for any contract args: contract(str): contract in the form NCD where N is a number 1-7, C is a letter (C/D/H/S/N) and D can be X or XX vulnerability(str): vulnerability for this board declarer(str): N/S/E/W tricks_taken(int): 0-13 """ # Get static for this session static = _score_for_contract_static(contract, vulnerability, declarer) # Making contracts if tricks_taken >= static.tricks_committed: return _score_for_contract_making(static, tricks_taken) # Non-making contracts else: return _score_for_contract_failing(static, tricks_taken)
[docs] def dealer_and_vulnerability_for_board(board_number): """return the vulnerability and dealer for a board""" # there are only 16 possibilities, for boards higher than 16 it just repeats board_number = board_number % 16 # deal rotates starting with North dealer = ["N", "E", "S", "W"][(board_number % 4) - 1] # Vul is random-ish # fmt: off vulnerabilities = ["Nil", "NS", "EW", "All", "NS", "EW", "All", "Nil", "EW", "All", "Nil", "NS", "All", "Nil", "NS", "EW", ] # fmt: on vulnerability = vulnerabilities[board_number - 1] return dealer, vulnerability
[docs] def is_making_contract(dds_table, contract, declarer): """Check in the dds table to see if this contract makes""" tricks = int(contract[0]) + 6 suit = contract[1] # ignore whether X or XX to check if making # This player can make it if dds_table[declarer][suit] >= tricks: return True # Their partner can make it if dds_table[partner_for(declarer)][suit] >= tricks: return True return False
[docs] def get_results_statistics(): """return basic stats on results. Called by utils statistics""" total_results = ResultsFile.objects.count() total_player_games = PlayerSummaryResult.objects.count() return { "total_results": total_results, "total_player_games": total_player_games, }