diff --git a/install/assemble.cmd b/install/assemble.cmd index 5761d121..e389269e 100644 --- a/install/assemble.cmd +++ b/install/assemble.cmd @@ -19,4 +19,6 @@ robocopy dist\BENGUI "BENAll" /E robocopy dist\BEN "BENAll" /E robocopy dist\table_manager_client "BENAll" /E robocopy ..\src\nn "BENAll\nn" *tf2.py* -robocopy ..\bin "BENAll\bin" /E \ No newline at end of file +robocopy ..\bin "BENAll\bin" /E +copy ..\src\ben.ico "BENAll" +copy ..\src\logo.png "BENAll" \ No newline at end of file diff --git a/src/bba/BBA.py b/src/bba/BBA.py index eb13627f..b4f949f9 100644 --- a/src/bba/BBA.py +++ b/src/bba/BBA.py @@ -2,7 +2,7 @@ import sys import os import util - +from threading import Lock # Get the directory of the current script script_dir = os.path.dirname(os.path.abspath(__file__)) # Calculate the parent directory @@ -34,6 +34,34 @@ class BBABotBid: + _dll_loaded = None # Class-level attribute to store the DLL singleton + _lock = Lock() # Lock to ensure thread-safe initialization + + @classmethod + def get_dll(cls, verbose = False): + """Access the loaded DLL classes.""" + if cls._dll_loaded is None: + with cls._lock: # Ensure only one thread can enter this block at a time + if cls._dll_loaded is None: # Double-checked locking + try: + util.load_dotnet_framework_assembly(EPBot_PATH, verbose) + from EPBot86 import EPBot + # Load the .NET assembly and import the types and classes from the assembly + if verbose: + print(f"EPBot Version (DLL): {EPBot().version()}") + cls._dll_loaded = { + "EPBot": EPBot, + } + except Exception as ex: + # Provide a message to the user if the assembly is not found + print(f"{Fore.RED}Error: Unable to load EPBot86.dll. Make sure the DLL is in the ./bin directory") + print("Make sure the dll is not blocked by OS (Select properties and click unblock)") + print(f"Make sure the dll is not writeprotected{Fore.RESET}") + print('Error:', ex) + sys.exit(1) + return cls._dll_loaded + + # Define constants for system types and conventions C_NS = 0 C_WE = 1 @@ -45,21 +73,10 @@ class BBABotBid: def __init__(self, ns_system, ew_system, position, hand, vuln, dealer, scoring_matchpoint, verbose): + dll = BBABotBid.get_dll(verbose) # Retrieve the loaded DLL classes through the singleton + EPBot = dll["EPBot"] self.verbose = verbose # Load the .NET assembly - try: - util.load_dotnet_framework_assembly(EPBot_PATH, verbose) - from EPBot86 import EPBot - # Load the .NET assembly and import the types and classes from the assembly - if self.verbose: - print(f"EPBot Version (DLL): {EPBot().version()}") - except Exception as ex: - # Provide a message to the user if the assembly is not found - print(f"{Fore.RED}Error: Unable to load EPBot86.dll. Make sure the DLL is in the ./bin directory") - print("Make sure the dll is not blocked by OS (Select properties and click unblock)") - print(f"Make sure the dll is not writeprotected{Fore.RESET}") - print('Error:', ex) - sys.exit(1) if ew_system == None and ns_system == None: return if ew_system == '-1' or ns_system == '-1': diff --git a/src/bots.py b/src/bots.py index 360606eb..335f1464 100644 --- a/src/bots.py +++ b/src/bots.py @@ -97,9 +97,8 @@ def get_binary_contract(self, position, vuln, hand_str, dummy_str, n_cards=32): def evaluate_rescue_bid(self, auction, passout, samples, candidate_bid, quality, my_bid_no ): # check configuration if self.verbose: - print("Checking if we should evaluate rescue bid", self.models.check_final_contract, len(samples)) - print("Auction",auction, passout) - print("Candidate", candidate_bid, quality) + print("Checking if we should evaluate rescue bid", self.models.check_final_contract, "Samples:",len(samples)) + print("Auction",auction, "Passout?" ,passout,"Candidate bid", candidate_bid.bid, "Sample quality: ",quality) if not self.models.check_final_contract: return False @@ -138,7 +137,7 @@ def evaluate_rescue_bid(self, auction, passout, samples, candidate_bid, quality, return True if candidate_bid.expected_score is None: - # We will prepare data for calculating recue bid + # We will prepare data for calculating rescue bid return True # We only evaluate if the score is below a certain value, so if simulation give a score above this we do not try to rescue @@ -231,7 +230,7 @@ def bid(self, auction): hands_np_as_pbn = self.translate_hands(hands_np, self.hand_str, sample_count) for candidate in candidates: if self.verbose: - print(f" {candidate.bid.ljust(4)} {candidate.insta_score:.3f} Samples: {len(hands_np)}") + print(f"Bid: {candidate.bid.ljust(4)} {candidate.insta_score:.3f}") auctions_np = self.bidding_rollout(auction, candidate.bid, hands_np) t_start = time.time() @@ -269,9 +268,9 @@ def bid(self, auction): # Format the average tricks string average_tricks_str = ", ".join(map(str, average_tricks)) - samples[idx] += f" \n {auc} ({average_tricks_str}) " + samples[idx] += f" | {auc} ({average_tricks_str}) " else: - samples[idx] += f" \n {auc}" + samples[idx] += f" | {auc}" if self.verbose: print("tricks", np.mean(decoded_tricks)) @@ -499,7 +498,7 @@ def bid(self, auction): result = {} for i in range(len(contracts[0])): # We should make calculations on this, so 4H, %h or even 6H is added, if tricks are fin - if contracts[0][i] > 0.3: + if contracts[0][i] > 0.2: y = np.zeros(5) suit = bidding.ID2BID[i][1] strain_i = 'NSHDC'.index(suit) @@ -532,15 +531,19 @@ def bid(self, auction): contract = bidding.ID2BID[contract_id] - if self.verbose: - print(result) if score < self.models.min_bidding_trust_for_sample_when_rescue: - if self.verbose: - print(self.hand_str, [sample[(self.seat + 2) % 4]]) - print(f"Skipping sample below level{self.models.min_bidding_trust_for_sample_when_rescue} {contract}-{tricks} score {score:.3f}") + #if self.verbose: + # print(self.hand_str, [sample[(self.seat + 2) % 4]]) + # if score == 0: + # print(f"No obvious rescue contract") + # else: + # print(f"Skipping sample below level: {self.models.min_bidding_trust_for_sample_when_rescue} {contract} {tricks} score {score:.3f}") continue + if self.verbose: + print(result) + while not bidding.can_bid(contract, auction) and contract_id < 35: contract_id += 5 contract = bidding.ID2BID[contract_id] @@ -587,8 +590,8 @@ def bid(self, auction): # If we go down we assume we are doubled doubled = tricks < level + 6 score = scoring.score(contract + ("X" if doubled else ""), self.vuln, tricks) - if self.verbose: - print(result, score) + #if self.verbose: + # print(result, score) if contract not in alternatives: alternatives[contract] = [] alternatives[contract].append({"score": score, "tricks": tricks}) @@ -1283,7 +1286,7 @@ def find_opening_lead(self, auction): opening_lead = candidate_cards[0].card.code() if self.verbose: - print("Samples quality:", quality) + print(f"Samples quality: {quality:.3f}") for card in candidate_cards: print(card) if opening_lead % 8 == 7: @@ -1427,7 +1430,7 @@ def double_dummy_estimates(self, lead_card_indexes, contract, accepted_samples): if (k != 3): hand_str += '.' if self.verbose: - print("Opening lead being examined: ", Card.from_code(opening_lead52), n_accepted) + print("Opening lead being examined: ", Card.from_code(opening_lead52), n_accepted, end="") t_start = time.time() hands_pbn = [] for i in range(n_accepted): @@ -1702,7 +1705,7 @@ def play_card(self, trick_i, leader_i, current_trick52, tricks52, players_states pimc_resp_cards = self.pimc.nextplay(self.player_i, shown_out_suits, self.missing_cards) if self.verbose: print("PIMCDef result:") - print("\n".join(f"{Card.from_code(k)}:\n{v}" for k, v in pimc_resp_cards.items())) + print("\n".join(f"{Card.from_code(k)}: {v}" for k, v in pimc_resp_cards.items())) assert pimc_resp_cards is not None, "PIMCDef result is None" if self.models.pimc_ben_dd_defending: @@ -1938,7 +1941,6 @@ def pick_card_after_pimc_eval(self, trick_i, leader_i, current_trick, tricks52, msg=msg + (f"|trump adjust={trump_adjust}" if trump_adjust != 0 and (card32 // 8) + 1 == self.strain_i else "") )) - if self.models.use_real_imp_or_mp: if self.models.matchpoint: candidate_cards = sorted(enumerate(candidate_cards), key=lambda x: (x[1].expected_score_mp, x[1].expected_tricks_dd, x[1].insta_score, -x[0]), reverse=True) @@ -1954,7 +1956,7 @@ def pick_card_after_pimc_eval(self, trick_i, leader_i, current_trick, tricks52, if self.verbose: for i in range(len(candidate_cards)): - print(candidate_cards[i].card, f"{candidate_cards[i].insta_score}:.3f", candidate_cards[i].expected_tricks_dd, round(5*candidate_cards[i].p_make_contract, 1), candidate_cards[i].expected_score_dd, int(candidate_cards[i].expected_tricks_dd * 10) / 10) + print(candidate_cards[i].card, f"{candidate_cards[i].insta_score:.3f}", candidate_cards[i].expected_tricks_dd, round(5 * candidate_cards[i].p_make_contract, 1), int(candidate_cards[i].expected_tricks_dd * 10) / 10) if self.models.matchpoint: if self.models.pimc_ben_dd_declaring or self.models.pimc_ben_dd_defending: diff --git a/src/carding.py b/src/carding.py index 8648da16..3a022ddf 100644 --- a/src/carding.py +++ b/src/carding.py @@ -165,7 +165,8 @@ def select_right_card_for_play(candidate_cards, rng, contract, models, hand_str, #print("Tricks", play['Plays'][0]['Tricks'], " Max: ",max(len(suits_north),len(suits_south))) if play['Plays'][0]['Tricks'] == max(len(suits_north),len(suits_south)): if play['Plays'][0]['Percentage'] == 100: - print(f"SuitC dropped as we can take all tricks {current_count} {original_count} ") + if verbose: + print(f"SuitC dropped as we can take all tricks {current_count} {original_count} ") return candidate_cards[0].card, who # We can have more than one play for MAX # So currently we are then selecting lowest card. Should that be different diff --git a/src/config/default.conf b/src/config/default.conf index 91602335..7c53cae6 100644 --- a/src/config/default.conf +++ b/src/config/default.conf @@ -1,5 +1,5 @@ [models] -name = GIB as on BBO +name = BEN Sayc # Model version 1 drops state for bidding, and introduce different system for NS and EW # Model version 2 includes 4 bids when making the lookup model_version = 3 @@ -19,8 +19,8 @@ use_bba = False # Instead of the neural network, check BBA if it is RKC and get the correct bid from BBA use_bba_to_count_aces = True # Use 2/1 in BBA -bba_ns = BBA/CC/GIB-BBO.bbsa -bba_ew = BBA/CC/GIB-BBO.bbsa +bba_ns = BBA/CC/BEN-SAYC.bbsa +bba_ew = BBA/CC/BEN-SAYC.bbsa # Playing matchpoint? Otherwise it is teams matchpoint = False # Do not print warnings about bad models, training etc in the output @@ -31,10 +31,10 @@ contract = models/TF2models/Contract_2024-12-09-E50.keras trick = models/TF2models/Tricks_2024-12-09-E50.keras [bidding] -bidder = models/TF2models/GIB-BBO-8712_2024-12-01-E44.keras +bidder = models/TF2models/BEN-Sayc-8712_2024-11-29-E49.keras # only used for sampling -opponent = models/TF2models/GIB-BBO-8712_2024-12-01-E44.keras -info = models/TF2models/GIB-BBOInfo-8712_2024-11-30-E50.keras +opponent = models/TF2models/BEN-Sayc-8712_2024-11-29-E49.keras +info = models/TF2models/BEN-SaycInfo-8712_2024-11-29-E71.keras # If there are multiple bids over this threshold make a simulation for the bids, using an array we can lower the trust the more we bid search_threshold = [0.10, 0.07, 0.06, 0.05, 0.04, 0.03, 0.03] # If there is bid above this threshold, make that bid ignoring other bids @@ -189,7 +189,7 @@ pimc_constraints_each_trick = True # Stop evaluation after finding the number of playouts unless pimc_wait expires before pimc_max_playouts = 200 # Max number of threads PIMC is allowed to use -pimc_max_threads = 6 +pimc_max_threads = 12 # If singleton, just play it without evaluating it autoplaysingleton = True # PIMC trust NN. We can filter away play, that is not suggested by the neural network diff --git a/src/config/default_tf2.conf b/src/config/default_tf2.conf index 3460888b..b1ac49f6 100644 --- a/src/config/default_tf2.conf +++ b/src/config/default_tf2.conf @@ -1,41 +1,53 @@ [models] -name = default (GIB) +name = BEN Sayc # Model version 1 drops state for bidding, and introduce different system for NS and EW # Model version 2 includes 4 bids when making the lookup model_version = 3 +# Small cards (pips) are grouped, so when bidding we have AKQJTx +n_cards_bidding = 24 +# During play we have AKQJT98x +n_cards_play = 32 + # Version of tensorflow to use tf_version = 2 -# Same model for both sides +# Same system for both sides NS = 1 EW = 1 # If using BBA the following NS and EW must match BBA's systems (0 =2/1, 1=SAYC, 2=WJ, 3=PC, 4=Acol) use_bba = False +# Instead of the neural network, check BBA if it is RKC and get the correct bid from BBA +use_bba_to_count_aces = True # Use 2/1 in BBA bba_ns = BBA/CC/BEN-21GF.bbsa bba_ew = BBA/CC/BEN-21GF.bbsa # Playing matchpoint? Otherwise it is teams matchpoint = False +# Do not print warnings about bad models, training etc in the output +suppress_warnings = True [contract] contract = models/TF2models/Contract_2024-12-09-E50.keras trick = models/TF2models/Tricks_2024-12-09-E50.keras - [bidding] -bidder = models/TF2models/GIB_2024-08-26-E50.keras -opponent = models/TF2models/GIB_2024-08-26-E50.keras -info = models/TF2models/GIB-Info_2024-07-18-E50.keras -# If there are multiple bids over this threshold make a simulation for the bids -search_threshold = 0.07 +bidder = models/TF2models/BEN-21GF-8712_2024-11-29-E48.keras +# only used for sampling +opponent = models/TF2models/BEN-21GF-8712_2024-11-29-E48.keras +info = models/TF2models/BEN-21GFInfo-8712_2024-11-29-E71.keras +# If there are multiple bids over this threshold make a simulation for the bids, using an array we can lower the trust the more we bid +search_threshold = [0.10, 0.07, 0.06, 0.05, 0.04, 0.03, 0.03] # If there is bid above this threshold, make that bid ignoring other bids no_search_threshold = 1 # Evaluate min_passout_candidates bids if auction longer than this. Setting to -1 will disable -eval_after_bid_count = 12 +# Disabled with a descending search_threshold +eval_after_bid_count = -1 # If False the opening bid will be without simulation, even with multiple candidates -eval_opening_bid = False +# With relatively high trust for first bid, this should be OK +eval_opening_bid = True # Add Pass as a bid to evaluate after this number of bids -eval_pass_after_bid_count = 12 +# Disabled when we have rescue bid activated +eval_pass_after_bid_count = -1 # Minimum number of candidates examined in the passout situation min_passout_candidates = 2 # Use bidding quality in evaluation (if not good just use neural network) @@ -52,15 +64,19 @@ max_samples_checked = 20 min_rescue_reward = 250 # Max expected score to evaluate rescue bid max_estimated_score = 300 +# If samples has bidding below this we will not use it for rescue bid +min_bidding_trust_for_sample_when_rescue = 0.55 # Alert implemented in bidding model -alert_supported = True +alert_supported = False +# We only alert if we are pretty sure +alert_threshold = 0.8 [adjustments] # Are adjustments enabled use_adjustment = True # Add extra weigth on scores from neural network # The score from NN is multiplied to this and added to expected score -adjust_NN = 50 +adjust_NN = 60 # If it was difficult to find samples we increase the trust in the NN adjust_NN_Few_Samples = 100 # subtract this from expected score before XX (Double if vulnerable) @@ -68,6 +84,8 @@ adjust_XX = 200 # Subtract this from expected score before double in passout situation # If not going 2 down, the adjustment is doubled adjust_X = 100 +# When doubling in passout situation remove xx% the best boards, due to bad samples +adjust_X_remove = 25 # When bidding in the passout situation, we change the expected score adjust_passout = -100 # When bidding in the passout and going negative, assume we are being doubled, so multiply the score by this @@ -78,6 +96,9 @@ adjust_min1 = 0.002 adjust_min2 = 0.0002 adjust_min1_by = 200 adjust_min2_by = 200 +# Adjustment above is in points, so we need to translate it to some kind of MP or Imp +factor_to_translate_to_mp = 10 +factor_to_translate_to_imp = 25 [lead] # Neural network for suggesting opening lead @@ -94,16 +115,20 @@ min_opening_leads = 4 # Opening lead agreement (random, attitude, 135 or 24) lead_from_pips_nt = 24 lead_from_pips_suit = 135 +# use real calcualtion or just tricks +use_real_imp_or_mp_opening_lead = True [eval] # Model for finding single dummy estimates provided an opening lead single_dummy_estimator = models/TF2models/SD_2024-07-08-E20.keras # Model for finding single dummy estimates without opening lead double_dummy_estimator = models/TF2models/RPDD_2024-07-08-E02.keras -# use the following estimator, sde, dde or both -estimator = dde -# Use double dummy when estimating tricks during bidding - to slow if 200 samples +# use the following estimator, sde, dde, both or none. If both dde will be preferred +estimator = none +# Use double dummy when estimating tricks during bidding - to slow if 200 samples - higher priority than the above estimators double_dummy_calculator = True +# use real calculation or just tricks - this is only working for double dummy calculator due to performance +use_real_imp_or_mp_bidding = True [cardplay] # This is telling if opening lead is included in the neural net for lefty @@ -119,87 +144,134 @@ decl_suit = models/TF2models/decl_suit_2024-07-08-E20.keras # Number of samples when playing the hand sample_hands_play = 200 # Minimum number of boards we want returned for sampling during play -min_sample_hands_play = 10 +min_sample_hands_play = 20 # Minimum number of boards we want returned for sampling during play min_sample_hands_play_bad = 12 # Number of samples we will generate to find sample_hands_play sample_boards_for_play = 5000 # Should possible claim be calculated claim = True -# Use bidding info during play +# Use bidding info for sampling during play and bidding +# Setting this to false will require a higher number of generated deals use_biddinginfo = True # Use bidding quality in evaluation, if bad samples, just use neural network use_biddingquality_in_eval = True # Use SuitC to find the card for single suit combinations use_suitc = True # Only check trump in suit contracts (and all suits in NT) -suitc_sidesuit_check = False +suitc_sidesuit_check = True +# Use real IMP or MP when finding the play - original version used average tricks +use_real_imp_or_mp = True # Drawing trump, reward is given if opponents has trump, penalty if not # The reward is low as it is only useable if more cards with the same score draw_trump_reward = 0.05 draw_trump_penalty = 0.1 +# We can filter away play, that is not suggested by the neural network. +trust_NN = 0.0 [pimc] -# Are we using PIMC as declarer or defender? +# Setting this to false will suppress output from PIMC, if true it will follow the global defintion of verbose +pimc_verbose = False +# Are we using PIMC as declarer and/or defender? pimc_use_declaring = True +pimc_use_fusion_strategy = False pimc_use_defending = True # Max wait time for results from PIMC in seconds pimc_wait = 3 -# When should PIMC kick in -pimc_start_trick_declarer = 3 -pimc_start_trick_defender = 6 +# When should PIMC kick in. PIMC is best with few cards unknown, but is used as a second opinion to BENs Double dummy +pimc_start_trick_declarer = 1 +pimc_start_trick_defender = 1 # Extract hcp from samples and use as input to PIMC pimc_constraints = True # On every trick create new constraints based on samples. If false only do it, when PIMC kicks in # The API is stateless and will establish constraints at each trick -pimc_constraints_each_trick = False +pimc_constraints_each_trick = True # Stop evaluation after finding the number of playouts unless pimc_wait expires before -pimc_max_playouts = 400 +pimc_max_playouts = 200 # Max number of threads PIMC is allowed to use -pimc_max_threads = 6 +pimc_max_threads = 12 # If singleton, just play it without evaluating it autoplaysingleton = True -# PIMC trust NN +# PIMC trust NN. We can filter away play, that is not suggested by the neural network pimc_trust_NN = 0.00 # Combine with BEN double dummy -pimc_ben_dd_declaring = False -pimc_ben_dd_defending = False +pimc_ben_dd_declaring = True +pimc_ben_dd_defending = True +# When merging results we can trust one side more. Default is 0.5. The weight is for PIMC, so the higher the more trust in PIMC +# If bidding quality is below the threshold, we will switch trust between BEN and PIMC, as BENs samples do not match the bidding. + +pimc_bidding_quality = 0.1 +pimc_ben_dd_declaring_weight = 0.3 +pimc_ben_dd_defending_weight = 0.3 # Use a priori probabilities -pimc_apriori_probability = True +# Setting this to true will send all played card to PIMC. that will give weight to each sample, when creating average tricks +# and when calculating chance for making/defeating the contract +pimc_apriori_probability = False +# These margins are added before calling OIMC to be sure we get some deals, that does not match the bidding entirely +pimc_margin_suit = 1 +pimc_margin_hcp = 3 +pimc_margin_suit_bad_samples = 2 +pimc_margin_hcp_bad_samples = 5 + [sampling] # When sampling we use bidding_info as a guide, and these control how to adjust bidding_info after a card given to a hand -# Multiplied to the hcp-value of the card +# Multiplied to the hcp-value of the card, the higher value there more the samples will have the average hcp's +# This can be disable by setting use_biddinginfo to false hcp_reduction_factor = 0.83 # when a card is given subtract this from the shape, to reduce odds it get more in that suit -shp_reduction_factor = 0.5 +shp_reduction_factor = 0.3 # If bid selected directly by Neural net, then save time dropping generating samples -no_samples_when_no_search = False +no_samples_when_no_search = True # Filter to remove hands, where the opening lead was not suggested by the neural network -# Can be disabled by setting it to zero -lead_accept_threshold = 0.03 -# If we play with our normal partner we can add som trust to the lead +# setting the following to false is the default behavior from a 32 card deck +# lead_accept_threshold_suit will sum all lead, including honors, for each suit and check the suit lead, adding up the score +lead_accept_threshold_suit = False +# lead_accept_threshold_honors will switch to the 24 deck suit, when validating the lead +# if following lead conventions like 2nd from 3 this should be enabled +lead_accept_threshold_honors = True +# Can be disabled by setting it to zero. This is a filter, so we keep ot low, as any lead can be possible +lead_accept_threshold = 0.10 +# Alternative is +# if following lead conventions like 2nd from 3 this should be enabled +# lead_accept_threshold_honors = False +# Can be disabled by setting it to zero. This is a filter, so we keep ot low, as any lead can be possible +# lead_accept_threshold = 0.01 + +# If we play with our normal partner we can add some trust to the lead # Setting it to high will reduce the samples and thus getting bad samples -lead_accept_threshold_partner_trust = 0.05 +lead_accept_threshold_partner_trust = 0.10 + # Filter to remove hands, where we do not trust the bidding. Used when sampling hands during bidding #use_distance = False #bidding_threshold_sampling = 0.25 #bid_accept_threshold_bidding = 0.20 +#bid_accept_play_threshold = 0.4 # This is calculated using euclidian distance, so 0.33 could be one of the 3 bids in a bidding round matching use_distance = True bidding_threshold_sampling = 0.70 +# If not finding enough samples above bidding_threshold_sampling we extend to this, until we get min_sample_hands_auction bid_accept_threshold_bidding = 0.40 # Filter to remove hands not consistent with the bidding. Used during play -bid_accept_play_threshold = 0.04 +bid_accept_play_threshold = 0.40 -# Filter to remove hands, where the play is inconsistent with the sample +# Filter to remove hands, where the play is inconsistent with the sample. Setting it to zero will remove the validation +# This is difficult, as setting it to low will include boards where BEN not will repeat a finesse, but setting it to high might reduce useable samples +# Also important to notice, this is not used for PIMC as defender or declarer play_accept_threshold = 0.02 +min_play_accept_threshold_samples = 10 # If we dont find enough samples, then include down to this threshold. Used during play bid_extend_play_threshold = 0.10 # Number hands where we will calculate scores for when bidding sample_hands_auction = 200 -# The number of hands we will generate to find sample_hands_auction -sample_boards_for_auction = 5000 +# The number of hands we will generate to find sample_hands_auction. Estimated cost pr. sample is 4ms +# This is now a max as we generate blocks of sample_boards_for_auction_step hands +sample_boards_for_auction = 20000 +sample_boards_for_auction_step = 1000 +# After each real bid we add 50% to the max samples. setting to zero disables +increase_for_bid_count = 6 +# If samples below this threshold print a warning in the log +warn_to_few_samples = 10 # Minimum number of hands when caclulating scores in the bidding min_sample_hands_auction = 10 # If we dont find enough samples, then include down to this threshold. Used during bidding @@ -211,4 +283,6 @@ sample_hands_opening_lead = 200 # Max number of samples to include when reviewing a board sample_hands_for_review = 20 # If probability for a bid is below this, then drop the sample -exclude_samples = 0.01 +exclude_samples = 0.005 +# Skip last bidding round to find samples, if we did not find the minimum needed. We will only try to find 25% samples +sample_previous_round_if_needed = True diff --git a/src/config/default_tf2_nosearch.conf b/src/config/default_tf2_nosearch.conf new file mode 100644 index 00000000..1d0f6b67 --- /dev/null +++ b/src/config/default_tf2_nosearch.conf @@ -0,0 +1,288 @@ +[models] +name = BEN Sayc +# Model version 1 drops state for bidding, and introduce different system for NS and EW +# Model version 2 includes 4 bids when making the lookup +model_version = 3 + +# Small cards (pips) are grouped, so when bidding we have AKQJTx +n_cards_bidding = 24 +# During play we have AKQJT98x +n_cards_play = 32 + +# Version of tensorflow to use +tf_version = 2 +# Same system for both sides +NS = 1 +EW = 1 +# If using BBA the following NS and EW must match BBA's systems (0 =2/1, 1=SAYC, 2=WJ, 3=PC, 4=Acol) +use_bba = False +# Instead of the neural network, check BBA if it is RKC and get the correct bid from BBA +use_bba_to_count_aces = False +# Use 2/1 in BBA +bba_ns = BBA/CC/BEN-21GF.bbsa +bba_ew = BBA/CC/BEN-21GF.bbsa +# Playing matchpoint? Otherwise it is teams +matchpoint = False +# Do not print warnings about bad models, training etc in the output +suppress_warnings = True + +[contract] +contract = models/TF2models/Contract_2024-12-09-E50.keras +trick = models/TF2models/Tricks_2024-12-09-E50.keras + +[bidding] +bidder = models/TF2models/BEN-21GF-8712_2024-11-29-E48.keras +# only used for sampling +opponent = models/TF2models/BEN-21GF-8712_2024-11-29-E48.keras +info = models/TF2models/BEN-21GFInfo-8712_2024-11-29-E71.keras +# If there are multiple bids over this threshold make a simulation for the bids, using an array we can lower the trust the more we bid +search_threshold = -1 +# If there is bid above this threshold, make that bid ignoring other bids +no_search_threshold = 1 +# Evaluate min_passout_candidates bids if auction longer than this. Setting to -1 will disable +# Disabled with a descending search_threshold +eval_after_bid_count = -1 +# If False the opening bid will be without simulation, even with multiple candidates +# With relatively high trust for first bid, this should be OK +eval_opening_bid = True +# Add Pass as a bid to evaluate after this number of bids +# Disabled when we have rescue bid activated +eval_pass_after_bid_count = -1 +# Minimum number of candidates examined in the passout situation +min_passout_candidates = 2 +# Use bidding quality in evaluation (if not good just use neural network) +use_biddingquality = True +# For very long bidding sequnces we can't find decent samples, so do not make quality check +no_biddingquality_after_bid_count = 12 +# Upvote the samples that matches the bidding best - used when double dummy results are calculated during play +use_probability = True +# Before the final pass validate the contract with a neural network +check_final_contract = False +# How many samples checked +max_samples_checked = 20 +# Minimum gain for making a rescue bid +min_rescue_reward = 250 +# Max expected score to evaluate rescue bid +max_estimated_score = 300 +# If samples has bidding below this we will not use it for rescue bid +min_bidding_trust_for_sample_when_rescue = 0.55 +# Alert implemented in bidding model +alert_supported = False +# We only alert if we are pretty sure +alert_threshold = 0.8 + +[adjustments] +# Are adjustments enabled +use_adjustment = True +# Add extra weigth on scores from neural network +# The score from NN is multiplied to this and added to expected score +adjust_NN = 60 +# If it was difficult to find samples we increase the trust in the NN +adjust_NN_Few_Samples = 100 +# subtract this from expected score before XX (Double if vulnerable) +adjust_XX = 200 +# Subtract this from expected score before double in passout situation +# If not going 2 down, the adjustment is doubled +adjust_X = 100 +# When doubling in passout situation remove xx% the best boards, due to bad samples +adjust_X_remove = 25 +# When bidding in the passout situation, we change the expected score +adjust_passout = -100 +# When bidding in the passout and going negative, assume we are being doubled, so multiply the score by this +adjust_passout_negative = 3 +# If we get some very low scores from the NN, then adjust by this +# Both will be tested, so a very low score will get both adjustments +adjust_min1 = 0.002 +adjust_min2 = 0.0002 +adjust_min1_by = 200 +adjust_min2_by = 200 +# Adjustment above is in points, so we need to translate it to some kind of MP or Imp +factor_to_translate_to_mp = 10 +factor_to_translate_to_imp = 25 + +[lead] +# Neural network for suggesting opening lead +lead_suit = models/TF2models/Lead-Suit_2024-11-04-E200.keras +lead_nt = models/TF2models/Lead-NT_2024-11-04-E200.keras +# Ignore cards as opening lead below this value from the neural network +lead_threshold = 0.20 +# Lead this card suggested by neural network if prediction is over this value +lead_accept_nn = 0.999 +# Use double dummy statistics when evaluating the opening lead - default is single dummy +double_dummy = True +# Force a minimum number of leads to consider - overrides lead_threshold +min_opening_leads = 4 +# Opening lead agreement (random, attitude, 135 or 24) +lead_from_pips_nt = 24 +lead_from_pips_suit = 135 +# use real calcualtion or just tricks +use_real_imp_or_mp_opening_lead = True + +[eval] +# Model for finding single dummy estimates provided an opening lead +single_dummy_estimator = models/TF2models/SD_2024-07-08-E20.keras +# Model for finding single dummy estimates without opening lead +double_dummy_estimator = models/TF2models/RPDD_2024-07-08-E02.keras +# use the following estimator, sde, dde, both or none. If both dde will be preferred +estimator = none +# Use double dummy when estimating tricks during bidding - to slow if 200 samples - higher priority than the above estimators +double_dummy_calculator = True +# use real calculation or just tricks - this is only working for double dummy calculator due to performance +use_real_imp_or_mp_bidding = True + +[cardplay] +# This is telling if opening lead is included in the neural net for lefty +opening_lead_included = True +lefty_nt = models/TF2models/lefty_nt_2024-07-08-E20.keras +dummy_nt = models/TF2models/dummy_nt_2024-07-08-E20.keras +righty_nt = models/TF2models/righty_nt_2024-07-16-E20.keras +decl_nt = models/TF2models/decl_nt_2024-07-08-E20.keras +lefty_suit = models/TF2models/lefty_suit_2024-07-08-E20.keras +dummy_suit = models/TF2models/dummy_suit_2024-07-08-E20.keras +righty_suit = models/TF2models/righty_suit_2024-07-16-E20.keras +decl_suit = models/TF2models/decl_suit_2024-07-08-E20.keras +# Number of samples when playing the hand +sample_hands_play = 200 +# Minimum number of boards we want returned for sampling during play +min_sample_hands_play = 20 +# Minimum number of boards we want returned for sampling during play +min_sample_hands_play_bad = 12 +# Number of samples we will generate to find sample_hands_play +sample_boards_for_play = 5000 +# Should possible claim be calculated +claim = True +# Use bidding info for sampling during play and bidding +# Setting this to false will require a higher number of generated deals +use_biddinginfo = True +# Use bidding quality in evaluation, if bad samples, just use neural network +use_biddingquality_in_eval = True +# Use SuitC to find the card for single suit combinations +use_suitc = True +# Only check trump in suit contracts (and all suits in NT) +suitc_sidesuit_check = True +# Use real IMP or MP when finding the play - original version used average tricks +use_real_imp_or_mp = True +# Drawing trump, reward is given if opponents has trump, penalty if not +# The reward is low as it is only useable if more cards with the same score +draw_trump_reward = 0.05 +draw_trump_penalty = 0.1 +# We can filter away play, that is not suggested by the neural network. +trust_NN = 0.0 + +[pimc] +# Setting this to false will suppress output from PIMC, if true it will follow the global defintion of verbose +pimc_verbose = False +# Are we using PIMC as declarer and/or defender? +pimc_use_declaring = True +pimc_use_fusion_strategy = False +pimc_use_defending = True +# Max wait time for results from PIMC in seconds +pimc_wait = 3 +# When should PIMC kick in. PIMC is best with few cards unknown, but is used as a second opinion to BENs Double dummy +pimc_start_trick_declarer = 1 +pimc_start_trick_defender = 1 +# Extract hcp from samples and use as input to PIMC +pimc_constraints = True +# On every trick create new constraints based on samples. If false only do it, when PIMC kicks in +# The API is stateless and will establish constraints at each trick +pimc_constraints_each_trick = True +# Stop evaluation after finding the number of playouts unless pimc_wait expires before +pimc_max_playouts = 200 +# Max number of threads PIMC is allowed to use +pimc_max_threads = 12 +# If singleton, just play it without evaluating it +autoplaysingleton = True +# PIMC trust NN. We can filter away play, that is not suggested by the neural network +pimc_trust_NN = 0.00 +# Combine with BEN double dummy +pimc_ben_dd_declaring = True +pimc_ben_dd_defending = True +# When merging results we can trust one side more. Default is 0.5. The weight is for PIMC, so the higher the more trust in PIMC +# If bidding quality is below the threshold, we will switch trust between BEN and PIMC, as BENs samples do not match the bidding. + +pimc_bidding_quality = 0.1 +pimc_ben_dd_declaring_weight = 0.3 +pimc_ben_dd_defending_weight = 0.3 +# Use a priori probabilities +# Setting this to true will send all played card to PIMC. that will give weight to each sample, when creating average tricks +# and when calculating chance for making/defeating the contract +pimc_apriori_probability = False +# These margins are added before calling OIMC to be sure we get some deals, that does not match the bidding entirely +pimc_margin_suit = 1 +pimc_margin_hcp = 3 +pimc_margin_suit_bad_samples = 2 +pimc_margin_hcp_bad_samples = 5 + + +[sampling] +# When sampling we use bidding_info as a guide, and these control how to adjust bidding_info after a card given to a hand +# Multiplied to the hcp-value of the card, the higher value there more the samples will have the average hcp's +# This can be disable by setting use_biddinginfo to false +hcp_reduction_factor = 0.83 +# when a card is given subtract this from the shape, to reduce odds it get more in that suit +shp_reduction_factor = 0.3 +# If bid selected directly by Neural net, then save time dropping generating samples +no_samples_when_no_search = True +# Filter to remove hands, where the opening lead was not suggested by the neural network +# setting the following to false is the default behavior from a 32 card deck +# lead_accept_threshold_suit will sum all lead, including honors, for each suit and check the suit lead, adding up the score +lead_accept_threshold_suit = False +# lead_accept_threshold_honors will switch to the 24 deck suit, when validating the lead +# if following lead conventions like 2nd from 3 this should be enabled +lead_accept_threshold_honors = True +# Can be disabled by setting it to zero. This is a filter, so we keep ot low, as any lead can be possible +lead_accept_threshold = 0.10 +# Alternative is +# if following lead conventions like 2nd from 3 this should be enabled +# lead_accept_threshold_honors = False +# Can be disabled by setting it to zero. This is a filter, so we keep ot low, as any lead can be possible +# lead_accept_threshold = 0.01 + +# If we play with our normal partner we can add some trust to the lead +# Setting it to high will reduce the samples and thus getting bad samples +lead_accept_threshold_partner_trust = 0.10 + +# Filter to remove hands, where we do not trust the bidding. Used when sampling hands during bidding +#use_distance = False +#bidding_threshold_sampling = 0.25 +#bid_accept_threshold_bidding = 0.20 +#bid_accept_play_threshold = 0.4 +# This is calculated using euclidian distance, so 0.33 could be one of the 3 bids in a bidding round matching +use_distance = True +bidding_threshold_sampling = 0.70 +# If not finding enough samples above bidding_threshold_sampling we extend to this, until we get min_sample_hands_auction +bid_accept_threshold_bidding = 0.40 +# Filter to remove hands not consistent with the bidding. Used during play +bid_accept_play_threshold = 0.40 + +# Filter to remove hands, where the play is inconsistent with the sample. Setting it to zero will remove the validation +# This is difficult, as setting it to low will include boards where BEN not will repeat a finesse, but setting it to high might reduce useable samples +# Also important to notice, this is not used for PIMC as defender or declarer +play_accept_threshold = 0.02 +min_play_accept_threshold_samples = 10 +# If we dont find enough samples, then include down to this threshold. Used during play +bid_extend_play_threshold = 0.10 +# Number hands where we will calculate scores for when bidding +sample_hands_auction = 200 +# The number of hands we will generate to find sample_hands_auction. Estimated cost pr. sample is 4ms +# This is now a max as we generate blocks of sample_boards_for_auction_step hands +sample_boards_for_auction = 20000 +sample_boards_for_auction_step = 1000 +# After each real bid we add 50% to the max samples. setting to zero disables +increase_for_bid_count = 6 +# If samples below this threshold print a warning in the log +warn_to_few_samples = 10 +# Minimum number of hands when caclulating scores in the bidding +min_sample_hands_auction = 10 +# If we dont find enough samples, then include down to this threshold. Used during bidding +bid_extend_bid_threshold = 0.01 +# How many boards should we sample to find the number of samples below +sample_boards_for_auction_opening_lead = 20000 +# Number of samples made, when finding the opening lead +sample_hands_opening_lead = 200 +# Max number of samples to include when reviewing a board +sample_hands_for_review = 20 +# If probability for a bid is below this, then drop the sample +exclude_samples = 0.005 +# Skip last bidding round to find samples, if we did not find the minimum needed. We will only try to find 25% samples +sample_previous_round_if_needed = True diff --git a/src/frontend/views/api.tpl b/src/frontend/views/api.tpl index 54c0674d..8110f4af 100644 --- a/src/frontend/views/api.tpl +++ b/src/frontend/views/api.tpl @@ -629,7 +629,7 @@ html += `

Samples(${data.samples.length}):

`; bids += 1; @@ -791,7 +791,7 @@ html += `

Samples(${data.samples.length}):

`; bids += 1; diff --git a/src/frontend/viz.html b/src/frontend/viz.html index ff5c88b0..fbeeb415 100644 --- a/src/frontend/viz.html +++ b/src/frontend/viz.html @@ -254,7 +254,7 @@ document.querySelector('#info').innerHTML += `

Samples(${data.samples.length}):

`; i += 1; diff --git a/src/frontend/viz.js b/src/frontend/viz.js index 73d8603c..2c49bef2 100644 --- a/src/frontend/viz.js +++ b/src/frontend/viz.js @@ -500,7 +500,7 @@ class PlayInfo { html += `

Samples(${this.data['samples'].length}):

` } diff --git a/src/game.py b/src/game.py index caf68867..eedec2f8 100644 --- a/src/game.py +++ b/src/game.py @@ -276,7 +276,9 @@ async def run(self, t_start): if self.verbose: for card_resp in self.card_responses: + print(f"{Fore.LIGHTCYAN_EX}") pprint.pprint(card_resp.to_dict(), width=200) + print(f"{Fore.RESET}") self.card_play = await self.play(self.contract, self.strain_i, self.decl_i, self.auction, opening_lead52) @@ -685,10 +687,10 @@ async def play(self, contract, strain_i, decl_i, auction, opening_lead52): card_resp.hcp = c_hcp card_resp.shape = c_shp if self.verbose: + print(f"{Fore.LIGHTCYAN_EX}") pprint.pprint(card_resp.to_dict(), width=200) + print(f"{Fore.RESET}") - if self.verbose: - print(f"{Fore.LIGHTCYAN_EX}{card_resp} selected by human{Fore.RESET}") await asyncio.sleep(0.01) self.card_responses.append(card_resp) @@ -886,7 +888,7 @@ async def opening_lead(self, auction): if self.human[(decl_i + 1) % 4]: card_resp = await self.factory.create_human_leader().async_lead() if self.verbose: - print(f"{Fore.LIGHTCYAN_EX}{card_resp} selected by human{Fore.RESET}") + print(f"{Fore.LIGHTCYAN_EX}{card_resp.card} selected by human{Fore.RESET}") else: bot_lead = AsyncBotLead( [self.vuln_ns, self.vuln_ew], @@ -1165,9 +1167,9 @@ async def main(): rdeal = random_deal_board(boardno) # example of to use a fixed deal - rdeal = ('T962.86.K742.643 KJ43.Q9.QT9.T852 .AJT7532.AJ8.QJ7 AQ875.K4.653.AK9', 'N E-W') + # rdeal = ('T962.86.K742.643 KJ43.Q9.QT9.T852 .AJT7532.AJ8.QJ7 AQ875.K4.653.AK9', 'N E-W') - print(f"Playing Board: {rdeal}") + print(f"{Fore.CYAN}Playing Board: {rdeal}{Fore.RESET}") driver.set_deal(None, *rdeal, False, bidding_only=biddingonly) else: rdeal = boards[board_no[0]]['deal'] diff --git a/src/gameapi.py b/src/gameapi.py index 37048d05..2d37f6d9 100644 --- a/src/gameapi.py +++ b/src/gameapi.py @@ -216,7 +216,9 @@ def play_api(dealer_i, vuln_ns, vuln_ew, hands, models, sampler, contract, strai card_resp.hcp = c_hcp card_resp.shape = c_shp if verbose: + print(f"{Fore.LIGHTCYAN_EX}") pprint.pprint(card_resp.to_dict(), width=200) + print(f"{Fore.RESET}") return card_resp, player_i @@ -629,7 +631,7 @@ def bid(): if record: calculations = {"hand":hand, "vuln":vuln, "dealer":dealer, "seat":seat, "auction":auction, "bid":bid.to_dict()} - logger.info(f"Calulations bid: {calculations}") + logger.info(f"Calulations bid: {json.dumps(calculations)}") print(f'Request took {(time.time() - t_start):0.2f} seconds') return json.dumps(result) except Exception as e: @@ -688,7 +690,7 @@ def lead(): result = card_resp.to_dict() if record: calculations = {"hand":hand, "vuln":vuln, "dealer":dealer, "seat":seat, "auction":auction, "lead":result} - logger.info(f"Calulations lead: {calculations}") + logger.info(f"Calulations lead: {json.dumps(calculations)}") print(f'Request took {(time.time() - t_start):0.2f} seconds') return json.dumps(result) except Exception as e: @@ -832,7 +834,7 @@ def print_trick(trick): result["MP_or_IMP"] = models.use_real_imp_or_mp if record: calculations = {"hand":hand_str, "dummy":dummy_str, "vuln":vuln, "dealer":dealer, "seat":seat, "auction":auction, "play":result} - logger.info(f"Calulations play: {calculations}") + logger.info(f"Calulations play: {json.dumps(calculations)}") print(f'Request took {(time.time() - t_start):0.2f} seconds') return json.dumps(result) except Exception as e: @@ -930,7 +932,7 @@ def cuebid(): result = {"bid": bid.bid.replace("PASS","Pass"), "alert": explanation, "artificial" : bid.alert} if record: calculations = {"hand":hand, "vuln":vuln, "dealer":dealer, "turn":turn, "auction":auction, "bid":bid.to_dict()} - logger.info(f"Calulations cuebid: {calculations}") + logger.info(f"Calulations cuebid: {json.dumps(calculations)}") print(f'Request took {(time.time() - t_start):0.2f} seconds') return json.dumps(result),200 diff --git a/src/pimc/PIMC.py b/src/pimc/PIMC.py index 301c3ad0..d31b2e43 100644 --- a/src/pimc/PIMC.py +++ b/src/pimc/PIMC.py @@ -2,6 +2,7 @@ import util import sys import os +from threading import Lock import math from objects import Card import time @@ -31,27 +32,57 @@ BGADLL_PATH = os.path.join(BIN_FOLDER, BGADLL_LIB) - class BGADLL: + _dll_loaded = None # Class-level attribute to store the DLL singleton + _lock = Lock() # Lock to ensure thread-safe initialization + + @classmethod + def get_dll(cls, verbose = False): + """Access the loaded DLL classes.""" + if cls._dll_loaded is None: + with cls._lock: # Ensure only one thread can enter this block at a time + if cls._dll_loaded is None: # Double-checked locking + try: + # Load the .NET assembly and import the types and classes from the assembly + util.load_dotnet_framework_assembly(BGADLL_PATH, verbose) + + from BGADLL import PIMC, Hand, Play, Constraints, Extensions, Macros, Card + + cls._dll_loaded = { + "PIMC": PIMC, + "Hand": Hand, + "Play": Play, + "Constraints": Constraints, + "Extensions": Extensions, + "Macros": Macros, + "PIMCCard": Card + } + + except Exception as ex: + # Provide a message to the user if the assembly is not found + print(f"{Fore.RED}Error: {ex}") + print("*****************************************************************************") + print("Error: Unable to load BGADLL.dll. Make sure the DLL is in the ./bin directory") + print("Make sure the dll is not blocked by OS (Select properties and click unblock)") + print("Make sure the dll is not write protected") + print(f"*****************************************************************************{Fore.RESET}") + sys.exit(1) + return cls._dll_loaded + def __init__(self, models, northhand, southhand, contract, is_decl_vuln, sampler, verbose): - try: - # Load the .NET assembly and import the types and classes from the assembly - util.load_dotnet_framework_assembly(BGADLL_PATH, verbose) - - from BGADLL import PIMC, Hand, Play, Constraints, Extensions, Card as PIMCCard - - except Exception as ex: - # Provide a message to the user if the assembly is not found - print(f"{Fore.RED}Error: {ex}") - print("*****************************************************************************") - print("Error: Unable to load BGADLL.dll. Make sure the DLL is in the ./bin directory") - print("Make sure the dll is not blocked by OS (Select properties and click unblock)") - print("Make sure the dll is not write protected") - print(f"*****************************************************************************{Fore.RESET}") - sys.exit(1) + dll = BGADLL.get_dll(verbose) # Retrieve the loaded DLL classes through the singleton + if dll is None: + raise RuntimeError("Failed to load BGADLL. Please ensure it is properly initialized.") if models == None: return + + # Access classes from the DLL + PIMC = dll["PIMC"] + Hand = dll["Hand"] + Play = dll["Play"] + Constraints = dll["Constraints"] + Extensions = dll["Extensions"] self.models = models self.sampler = sampler self.max_playout = models.pimc_max_playouts @@ -94,7 +125,8 @@ def calculate_hcp(self, rank): return hcp_values.get(rank, 0) def reset_trick(self): - from BGADLL import Play + dll = BGADLL.get_dll() # Access the loaded DLL singleton + Play = dll["Play"] # Retrieve the Play class from the DLL self.previous_tricks.AddRange(self.current_trick) self.current_trick = Play() @@ -127,9 +159,9 @@ def set_shape_constraints(self, min_lho, max_lho, min_rho, max_rho, quality): print("already_shown_lho",self.already_shown_lho) print("already_shown_rho",self.already_shown_rho) - if self.verbose: - print("LHO", min_lho, max_lho) - print("RHO", min_rho, max_rho) + #if self.verbose: + # print("LHO", min_lho, max_lho) + # print("RHO", min_rho, max_rho) for i in range(4): # If samples show 5-card+ we only reduce by 1 @@ -152,9 +184,9 @@ def set_shape_constraints(self, min_lho, max_lho, min_rho, max_rho, quality): max_rho[i] = min(max_rho[i] + margin - self.already_shown_rho[i], 13) - if self.verbose: - print("LHO", min_lho, max_lho) - print("RHO", min_rho, max_rho) + #if self.verbose: + # print("LHO", min_lho, max_lho) + # print("RHO", min_rho, max_rho) self.lho_constraints.MinSpades = int(min_lho[0]) self.lho_constraints.MinHearts = int(min_lho[1]) @@ -251,7 +283,8 @@ def set_card_played(self, card52, playedBy, openinglead): print(f"Setting card {real_card} played by {playedBy} for PIMC") card = real_card.symbol_reversed() - from BGADLL import Card as PIMCCard + dll = BGADLL.get_dll() # Access the loaded DLL singleton + PIMCCard = dll["PIMCCard"] # Retrieve the Card class from the DLL self.current_trick.Add(PIMCCard(card)) self.opposHand.Remove(PIMCCard(card)) suit = real_card.suit @@ -273,7 +306,8 @@ def set_card_played(self, card52, playedBy, openinglead): self.update_constraints(playedBy, real_card) def find_trump(self, value): - from BGADLL import Macros + dll = BGADLL.get_dll() # Access the loaded DLL singleton + Macros = dll["Macros"] # Retrieve the Card class from the DLL if value == 4: return Macros.Trump.Club elif value == 3: @@ -343,7 +377,9 @@ def update_voids(self,shown_out_suits): # Define a Python function to find a bid def nextplay(self, player_i, shown_out_suits, missing_cards): - from BGADLL import Constraints, Macros, Card as PIMCCard + dll = BGADLL.get_dll() # Access the loaded DLL singleton + Constraints = dll["Constraints"] # Retrieve the Card class from the DLL + Macros = dll["Macros"] # Retrieve the Card class from the DLL try: self.pimc.Clear() @@ -365,8 +401,7 @@ def nextplay(self, player_i, shown_out_suits, missing_cards): print(self.opposHand.ToString(), self.current_trick.ListAsString()) print("Voids:", shown_out_suits) print(Macros.Player.South if player_i == 3 else Macros.Player.North) - print("Tricks taken", self.tricks_taken) - print("min tricks",self.mintricks) + print("Tricks taken:", self.tricks_taken, "Tricks needed:",self.mintricks) print("East (RHO)",self.rho_constraints.ToString()) print("West (LHO)",self.lho_constraints.ToString()) print("Autoplay",self.autoplay) @@ -386,8 +421,7 @@ def nextplay(self, player_i, shown_out_suits, missing_cards): print(self.opposHand.ToString(), self.current_trick.ListAsString()) print("Voids:", shown_out_suits) print(Macros.Player.South if player_i == 3 else Macros.Player.North) - print("Tricks taken", self.tricks_taken) - print("min tricks",self.mintricks) + print("Tricks taken:", self.tricks_taken, "Tricks needed:",self.mintricks) print("East (RHO)",self.rho_constraints.ToString()) print("West (LHO)",self.lho_constraints.ToString()) print("Current trick",self.current_trick.ListAsString()) @@ -456,7 +490,7 @@ def nextplay(self, player_i, shown_out_suits, missing_cards): if self.verbose: print(f"max_playouts: {self.max_playout} Playouts: {self.pimc.Playouts} Combinations: {self.pimc.Combinations} Examined: {self.pimc.Examined}") print(self.northhand.ToString(), self.southhand.ToString(), self.opposHand.ToString(), self.current_trick.ListAsString()) - print("Trump:",trump,"Tricks taken",self.tricks_taken,"min tricks",self.mintricks) + print("Trump:",trump,"Tricks taken:",self.tricks_taken,"Tricks needed:",self.mintricks) print("Voids",shown_out_suits) print("East (RHO)", self.rho_constraints.ToString(),"West (LHO)", self.lho_constraints.ToString()) #print("Current trick",self.current_trick.ListAsString()) @@ -533,7 +567,7 @@ def nextplay(self, player_i, shown_out_suits, missing_cards): card_result = {} for key in card_ev.keys(): - card_result[key] = (round(e_tricks[key], 2), round(card_ev[key],2), making[key], msg) + card_result[key] = (round(e_tricks[key], 2), round(card_ev[key],2), round(making[key],3), msg) if self.verbose: print(f'{Card.from_code(key)} {round(e_tricks[key],3):0.3f} {round(card_ev[key],2):5.2f} {round(making[key],3):0.3f}') diff --git a/src/pimc/PIMCDef.py b/src/pimc/PIMCDef.py index 3a8d6a96..1bd4b6f0 100644 --- a/src/pimc/PIMCDef.py +++ b/src/pimc/PIMCDef.py @@ -2,6 +2,7 @@ import util import sys import os +from threading import Lock import math from objects import Card import time @@ -34,24 +35,55 @@ class BGADefDLL: + _dll_loaded = None # Class-level attribute to store the DLL singleton + _lock = Lock() # Lock to ensure thread-safe initialization + + @classmethod + def get_dll(cls, verbose = False): + """Access the loaded DLL classes.""" + if cls._dll_loaded is None: + with cls._lock: # Ensure only one thread can enter this block at a time + if cls._dll_loaded is None: # Double-checked locking + try: + # Load the .NET assembly and import the types and classes from the assembly + util.load_dotnet_framework_assembly(BGADLL_PATH, verbose) + + from BGADLL import PIMCDef, Hand, Play, Constraints, Extensions, Macros, Card + + cls._dll_loaded = { + "PIMCDef": PIMCDef, + "Hand": Hand, + "Play": Play, + "Constraints": Constraints, + "Extensions": Extensions, + "Macros": Macros, + "PIMCCard": Card + } + + except Exception as ex: + # Provide a message to the user if the assembly is not found + print(f"{Fore.RED}Error: {ex}") + print("*****************************************************************************") + print("Error: Unable to load BGADLL.dll. Make sure the DLL is in the ./bin directory") + print("Make sure the dll is not blocked by OS (Select properties and click unblock)") + print("Make sure the dll is not write protected") + print(f"*****************************************************************************{Fore.RESET}") + sys.exit(1) + return cls._dll_loaded + def __init__(self, models, northhand, southhand, contract, is_decl_vuln, player_i, sampler, verbose): - try: - # Load the .NET assembly and import the types and classes from the assembly - util.load_dotnet_framework_assembly(BGADLL_PATH, verbose) - from BGADLL import PIMCDef, Hand, Constraints, Extensions, Play - - except Exception as ex: - # Provide a message to the user if the assembly is not found - print(f"{Fore.RED}Error:", ex) - print("*****************************************************************************") - print("Error: Unable to load BGADLL.dll. Make sure the DLL is in the ./bin directory") - print("Make sure the dll is not blocked by OS (Select properties and click unblock)") - print("Make sure the dll is not write protected") - print(f"*****************************************************************************{Fore.RESET}") - sys.exit(1) + dll = BGADefDLL.get_dll(verbose) # Retrieve the loaded DLL classes through the singleton + if dll is None: + raise RuntimeError("Failed to load BGADLL. Please ensure it is properly initialized.") if models == None: return + # Access classes from the DLL + PIMCDef = dll["PIMCDef"] + Hand = dll["Hand"] + Play = dll["Play"] + Constraints = dll["Constraints"] + Extensions = dll["Extensions"] self.models = models self.sampler = sampler self.max_playout = models.pimc_max_playouts @@ -67,6 +99,7 @@ def __init__(self, models, northhand, southhand, contract, is_decl_vuln, player_ self.opposHand = self.full_deck.Except(self.dummyhand.Union(self.defendinghand)) self.current_trick = Play() self.previous_tricks = Play() + # Constraint are Clubs, Diamonds ending with hcp self.partner_constraints = Constraints(0, 13, 0, 13, 0, 13, 0, 13, 0, 37) self.declarer_constraints = Constraints(0, 13, 0, 13, 0, 13, 0, 13, 0, 37) @@ -93,7 +126,8 @@ def calculate_hcp(self, rank): return hcp_values.get(rank, 0) def reset_trick(self): - from BGADLL import Play + dll = BGADefDLL.get_dll() # Access the loaded DLL singleton + Play = dll["Play"] # Retrieve the Play class from the DLL self.previous_tricks.AddRange(self.current_trick) self.current_trick = Play() @@ -149,8 +183,8 @@ def set_shape_constraints(self, min_partner, max_partner, min_declarer, max_decl max_declarer[i] = min(max_declarer[i] + margin_declarer - self.already_shown_declarer[i], 13) - if self.verbose: - print(min_partner, max_partner, min_declarer, max_declarer) + #if self.verbose: + # print(min_partner, max_partner, min_declarer, max_declarer) self.partner_constraints.MinSpades = int(min_partner[0]) self.partner_constraints.MinHearts = int(min_partner[1]) @@ -182,8 +216,8 @@ def set_hcp_constraints(self, min_partner, max_partner, min_declarer, max_declar # Constraints are for the remaining tricks and input if for full samples, so we subtract previous played card if self.verbose: print(min_partner, max_partner, min_declarer, max_declarer, quality) - print("already_shown_d",self.already_shown_hcp_declarer) - print("already_shown_p",self.already_shown_hcp_partner) + print("already_shown_declarer",self.already_shown_hcp_declarer) + print("already_shown_partner",self.already_shown_hcp_partner) if quality: margin = self.models.pimc_margin_hcp else: @@ -248,7 +282,8 @@ def set_card_played(self, card52, playedBy, openinglead): print(f"Setting card {real_card} played by {playedBy} for PIMCDef") card = real_card.symbol_reversed() - from BGADLL import Card as PIMCCard + dll = BGADefDLL.get_dll() # Access the loaded DLL singleton + PIMCCard = dll["PIMCCard"] # Retrieve the Card class from the DLL self.current_trick.Add(PIMCCard(card)) self.opposHand.Remove(PIMCCard(card)) suit = real_card.suit @@ -278,7 +313,8 @@ def set_card_played(self, card52, playedBy, openinglead): self.update_constraints(playedBy, real_card) def find_trump(self, value): - from BGADLL import Macros + dll = BGADefDLL.get_dll() # Access the loaded DLL singleton + Macros = dll["Macros"] # Retrieve the Card class from the DLL if value == 4: return Macros.Trump.Club elif value == 3: @@ -375,8 +411,7 @@ def nextplay(self, player_i, shown_out_suits, missing_cards): print("Voids:", shown_out_suits) print(Macros.Player.West if player_i == 0 else Macros.Player.East) print("Over dummy", self.player_i == 2) - print("Tricks taken", self.tricks_taken) - print("min tricks",self.mintricks) + print("Tricks taken:", self.tricks_taken, "Tricks needed:",self.mintricks) print("Declarer",self.declarer_constraints.ToString()) print("Partner",self.partner_constraints.ToString()) print("Autoplay",self.autoplay) @@ -397,8 +432,7 @@ def nextplay(self, player_i, shown_out_suits, missing_cards): print(Macros.Player.West if player_i == 0 else Macros.Player.East) print("self.player_i",self.player_i) print("Over dummy", self.player_i == 2) - print("Tricks taken", self.tricks_taken) - print("min tricks",self.mintricks) + print("Tricks taken:", self.tricks_taken, "Tricks needed:",self.mintricks) print("Declarer",self.declarer_constraints.ToString()) print("Partner",self.partner_constraints.ToString()) print("Current trick",self.current_trick.ListAsString()) @@ -467,7 +501,7 @@ def nextplay(self, player_i, shown_out_suits, missing_cards): if self.verbose: print(f"Max_playout: {self.max_playout} Playouts: {self.pimc.Playouts} Combinations: {self.pimc.Combinations} Examined:{self.pimc.Examined}") print(self.dummyhand.ToString(), self.defendinghand.ToString(), self.opposHand.ToString(), self.current_trick.ListAsString()) - print("Trump:",trump, "min tricks",self.mintricks) + print("Trump:",trump,"Tricks taken:",self.tricks_taken,"Tricks needed:",self.mintricks) print("Voids",shown_out_suits) print(Macros.Player.West if player_i == 0 else Macros.Player.East) print("self.player_i",self.player_i) @@ -546,7 +580,7 @@ def nextplay(self, player_i, shown_out_suits, missing_cards): card_result = {} for key in card_ev.keys(): - card_result[key] = (round(e_tricks[key], 2), round(card_ev[key],2), making[key], msg) + card_result[key] = (round(e_tricks[key], 2), round(card_ev[key],2), round(making[key]), msg) if self.verbose: print(f'{Card.from_code(key)} {round(e_tricks[key],3):0.3f} {round(card_ev[key],2):5.2f} {round(making[key],3):0.3f}') diff --git a/src/sample.py b/src/sample.py index c4f6cc1c..e687954e 100644 --- a/src/sample.py +++ b/src/sample.py @@ -169,7 +169,7 @@ def generate_samples_iterative(self, auction_so_far, turn_to_bid, max_samples, n step = self.sample_boards_for_auction_step extend_samples = False while samplings < sample_boards_for_auction and current_count <= needed_samples: - new_samples, new_sorted_scores, p_hcp, p_shp, new_quality = self.sample_cards_auction(auction_so_far, turn_to_bid, hand_str, vuln, step, rng, models, p_hcp, p_shp, extend_samples ) + new_samples, new_sorted_scores, p_hcp, p_shp, new_quality = self.sample_cards_auction(auction_so_far, turn_to_bid, hand_str, vuln, step, rng, models, p_hcp, p_shp, extend_samples, self.verbose and current_count == 0) current_count += new_samples.shape[0] quality += new_quality * new_samples.shape[0] # Accumulate the samples and scores @@ -235,7 +235,7 @@ def generate_samples_iterative(self, auction_so_far, turn_to_bid, max_samples, n def sample_cards_vec(self, n_samples, c_hcp, c_shp, my_hand, rng, n_cards=32): if self.verbose: - print("sample_cards_vec generating", n_samples, rng.bit_generator.state['state']['state']) + print("sample_cards_vec generating", n_samples, "Seed:", rng.bit_generator.state['state']['state']) t_start = time.time() deck = np.ones(n_cards, dtype=int) cards_in_suit = n_cards // 4 @@ -298,11 +298,9 @@ def sample_cards_vec(self, n_samples, c_hcp, c_shp, my_hand, rng, n_cards=32): if self.verbose: - print("Missing HCP :", missing_hcp) - print("Expected HCP :",r_hcp[0]) - print("Expected Shape:",r_shp[0]) - print(f"hcp_reduction_factor:{hcp_reduction_factor} {self.hcp_reduction_factor}") - print(f"shp_reduction_factor:{shp_reduction_factor} {self.shp_reduction_factor}") + print("Missing HCP :", missing_hcp,"Expected HCP :",r_hcp[0],"Expected Shape:","".join(map(str, r_shp[0])), + f"hcp_reduction_factor:{hcp_reduction_factor} {self.hcp_reduction_factor}", + f"shp_reduction_factor:{shp_reduction_factor} {self.shp_reduction_factor}") # all AK's in the same hand # vectorize has an overhead @@ -357,8 +355,8 @@ def sample_cards_vec(self, n_samples, c_hcp, c_shp, my_hand, rng, n_cards=32): # print("Loop counter >= 76") # break # - if self.verbose: - print("Loops to deal the hands", loop_counter) + #if self.verbose: + # print("Loops to deal the hands", loop_counter) if self.use_bidding_info: @@ -466,11 +464,11 @@ def process_bidding(self, player, lho_pard_rho, min_scores_lho, min_scores_partn return lho_pard_rho, min_scores_lho, min_scores_partner, min_scores_rho - def sample_cards_auction(self, auction, nesw_i, hand_str, vuln, n_samples, rng, models, old_c_hcp, old_c_shp, extend_samples = True): + def sample_cards_auction(self, auction, nesw_i, hand_str, vuln, n_samples, rng, models, old_c_hcp, old_c_shp, extend_samples = True, verbose = False): hand = binary.parse_hand_f(models.n_cards_bidding)(hand_str) n_steps = binary.calculate_step_bidding_info(auction) bids = 4 if models.model_version >= 2 else 3 - if self.verbose: + if verbose: print("sample_cards_auction, nsteps=", n_steps) print("NS: ", models.ns, "EW: ", models.ew, "Auction: ", auction) print("hand", hand_str) @@ -489,22 +487,16 @@ def sample_cards_auction(self, auction, nesw_i, hand_str, vuln, n_samples, rng, # Consider saving the generated boards, and add the result from previous sampling to this output n_samples = lho_pard_rho.shape[0] - if self.verbose: + if verbose: print(f"n_samples {n_samples} from bidding info") n_steps = binary.calculate_step_bidding(auction) - if self.verbose: - print("n_steps", n_steps) #print(auction) index = 0 if models.model_version == 0 or models.ns == -1 else 2 size = 7 + models.n_cards_bidding + index + bids*40 - - - if self.verbose: - print("get_bid_ids", n_steps, auction) # Initialize the players list with a loop to get bid information for each position players = [] @@ -578,7 +570,7 @@ def sample_cards_auction(self, auction, nesw_i, hand_str, vuln, n_samples, rng, # print(i, distances[i], hand_to_str(lho_pard_rho[i, 0:1, :], models.n_cards_bidding), round(abs_diff_lho,3), hand_to_str(lho_pard_rho[i, 1:2, :], models.n_cards_bidding),round(abs_diff_partner,3), hand_to_str(lho_pard_rho[i, 2:3, :], models.n_cards_bidding),round(abs_diff_rho,3)) # Normalize the total distance to a scale between 0 and 100 - if self.verbose: + if verbose: print("Max distance", max_distance, lho_bid_count, pard_bid_count, rho_bid_count) scaled_distance_A = ((max_distance - distances) / max_distance) #print("scaled_distance_A", scaled_distance_A) @@ -605,32 +597,32 @@ def sample_cards_auction(self, auction, nesw_i, hand_str, vuln, n_samples, rng, sorted_samples = sorted_samples[sorted_scores >= 0] sorted_scores = sorted_scores[sorted_scores >= 0] - if self.verbose: + if verbose: print("Samples after bidding distance: ", len(sorted_samples), " Threshold: ") if len(sorted_scores) == 0: return sorted_samples, sorted_scores, c_hcp, c_shp, -1 # How much to trust the bidding for the samples accepted_samples = sorted_samples[sorted_scores >= self.bidding_threshold_sampling] - if self.verbose: + if verbose: print("Samples after bidding filtering: ", len(accepted_samples), " Threshold: ", self.bidding_threshold_sampling) # If we havent found enough samples, just return the minimum number from configuration if (len(accepted_samples) < self._min_sample_hands_auction) and extend_samples: if self.use_distance: - if self.verbose: + if verbose: print(f"Only found {len(sorted_samples[sorted_scores >= self.bid_accept_threshold_bidding])} {self._min_sample_hands_auction}") else: - if self.verbose: + if verbose: print(f"Only found {len(accepted_samples)} {self._min_sample_hands_auction}") accepted_samples = sorted_samples[:self._min_sample_hands_auction] if len(accepted_samples) == 0: - if self.verbose: + if verbose: print(f"No samples found. Extend={extend_samples}") return accepted_samples, [], c_hcp, c_shp, -1 - if self.verbose: + if verbose: print("Returning", len(accepted_samples), extend_samples) sorted_scores = sorted_scores[:len(accepted_samples)] quality = np.mean(sorted_scores) @@ -652,6 +644,7 @@ def shuffle_cards_bidding_info(self, n_samples, auction, hand_str, public_hand_s h1_passive = all(item in {"PASS", "PAD_START"} for item in auction[h_1_nesw::4]) h2_passive = all(item in {"PASS", "PAD_START"} for item in auction[h_2_nesw::4]) + #print(f"h1_passive: {h1_passive}, h2_passive: {h2_passive}") use_bidding_info_stats = self.use_bidding_info and not (h1_passive and h2_passive) if use_bidding_info_stats: @@ -680,8 +673,6 @@ def f_trans_shp(x): return 1.75 * x + 3.25 # we need to count the hcp missing and compare it to stats missing_hcp = 40 - (binary.get_hcp(hand)[0] + binary.get_hcp(binary.parse_hand_f(models.n_cards_bidding)(public_hand_str))[0]) - if self.verbose: - print("missing_hcp:", missing_hcp) if missing_hcp > 0: hcp_reduction_factor = round(self.hcp_reduction_factor * np.sum(p_hcp) / missing_hcp, 2) else: @@ -690,7 +681,7 @@ def f_trans_shp(x): return 1.75 * x + 3.25 shp_reduction_factor = self.shp_reduction_factor if self.verbose: - print("hcp_reduction_factor",hcp_reduction_factor, "shp_reduction_factor" ,shp_reduction_factor) + print("missing_hcp:", missing_hcp, "hcp_reduction_factor",hcp_reduction_factor, "shp_reduction_factor" ,shp_reduction_factor) # acknowledge all played cards for i, cards in enumerate(cards_played): @@ -711,7 +702,7 @@ def f_trans_shp(x): return 1.75 * x + 3.25 r_shp = np.zeros((n_samples, 2, 4)) + p_shp else: if self.verbose: - print("We do not use the bidding info") + print(f"We do not use the bidding info. Opponents passive={h1_passive and h2_passive}") shp_reduction_factor = 0 hcp_reduction_factor = 0 # we do not use the bidding info @@ -889,10 +880,7 @@ def get_opening_lead_scores(self, auction, vuln, models, handsamples, opening_le def get_bid_scores(self, nesw_i, partner, auction, vuln, sample_hands, models): n_steps = binary.calculate_step_bidding(auction) if self.verbose: - print(f"{Fore.YELLOW}sample hand {hand_to_str(sample_hands[0])}{Fore.RESET}") - print("n_step", n_steps) - print("auction", auction) - print("nesw_i", nesw_i) + print(f"Get bid scores for samples. First hand {hand_to_str(sample_hands[0])}") # convert the deck if different for play and bidding if models.n_cards_play != models.n_cards_bidding: @@ -936,8 +924,8 @@ def init_rollout_states_iterative(self, trick_i, player_i, card_players, player_ hand_bidding = binary.parse_hand_f(models.n_cards_bidding)(hand_str) n_samples = self.sample_hands_play contract = bidding.get_contract(auction) - if self.verbose: - print(f"Called init_rollout_states_iterative {n_samples} - Contract {contract} - Player {player_i}") + #if self.verbose: + # print(f"Called init_rollout_states_iterative {n_samples} - Contract {contract} - Player {player_i}") leader_i = (player_i - len(current_trick)) % 4 # Dummy is always 1 @@ -1282,12 +1270,10 @@ def f_trans_shp(x): return 1.75 * x + 3.25 def validate_opening_lead_for_sample(self, trick_i, hidden_1_i, hidden_2_i, current_trick, player_cards_played, models, auction, vuln, states, bid_scores): if self.verbose: - print("validate_opening_lead_for_sample") - print("lead_accept_threshold", self.lead_accept_threshold) - print("lead_accept_threshold_partner_trust", self.lead_accept_threshold_partner_trust) - print("min_sample_hands_play", self.min_sample_hands_play) - print("lead_accept_threshold_suit",self.lead_accept_threshold_suit) - print("lead_accept_threshold_honors",self.lead_accept_threshold_honors) + print("validate_opening_lead_for_sample:", "lead_accept_threshold:", self.lead_accept_threshold, + "lead_accept_threshold_partner_trust:", self.lead_accept_threshold_partner_trust, + "min_sample_hands_play:", self.min_sample_hands_play, "lead_accept_threshold_suit:", self.lead_accept_threshold_suit, + "lead_accept_threshold_honors:", self.lead_accept_threshold_honors) # Only make the test if opening leader (0) is hidden # The primary idea is to filter away hands, that lead the Q as it denies the K lead_scores = np.zeros(states[0].shape[0]) @@ -1334,6 +1320,7 @@ def validate_opening_lead_for_sample(self, trick_i, hidden_1_i, hidden_2_i, curr # Check that the play until now is expected with the samples # In principle we do this to eliminated hands, where the card played is inconsistent with the sample # We should probably only validate partner as he follow our rules (what is in the neural net) + # But it will also help us in eleminating hands, where the play would be illogical def validate_play_until_now(self, trick_i, current_trick, leader_i, player_cards_played, hidden_1_i, hidden_2_i, states, bidding_scores, models, contract, lead_scores): # If they don't cover they dont have that card # Should be implemented as a logical rule TODO @@ -1373,17 +1360,22 @@ def validate_play_until_now(self, trick_i, current_trick, leader_i, player_cards print(f"cards_played by {p_i} {cards_played}") # We have 11 rounds of play in the neural network, but might have only 10 for Lefty if p_i == 0 and not models.opening_lead_included: - n_tricks_pred = trick_i + len(card_played_current_trick) + n_tricks_pred = trick_i + len(card_played_current_trick) - 1 else: n_tricks_pred = trick_i + len(card_played_current_trick) # Depending on suit or NT we must select the right model # 0-3 is for NT 4-7 is for suit # When the player is instantiated the right model is selected, but here we get it from the configuration + # print("trick_i", trick_i, "len(card_played_current_trick)", len(card_played_current_trick), "n_tricks_pred", n_tricks_pred) p_cards = models.player_models[p_i+playermodelindex].pred_fun(states[p_i][:, :n_tricks_pred, :]) if tf.is_tensor(p_cards): p_cards = p_cards.numpy() card_scores = p_cards[:, np.arange(len(cards_played)), cards_played] + #print(card_scores.shape) + #x = states[p_i][:, :n_tricks_pred, :] + #for i in range(max(10,x.shape[0])): + # print(hand_to_str(x[i,0,0:32],models.n_cards_play), card_scores[i, :]) # The opening lead is validated elsewhere, so we just change the score to 1 for all samples if p_i == 0 and models.opening_lead_included: card_scores[:, 0] = 1 @@ -1391,6 +1383,7 @@ def validate_play_until_now(self, trick_i, current_trick, leader_i, player_cards min_play_scores = np.minimum(min_play_scores, np.min(card_scores, axis=1)) + #print(f"card_scores combined {min_play_scores}") # Trust in the play until now play_accept_threshold = self.play_accept_threshold diff --git a/src/util.py b/src/util.py index 8b25f1a2..478654dd 100644 --- a/src/util.py +++ b/src/util.py @@ -1,4 +1,5 @@ import sys +import os import numpy as np import re import hashlib @@ -259,6 +260,25 @@ def get_possible_cards(hand, current_trick): result = check_sequence(cards, suitlead) return result +def check_file_blocked(file_path): + blocked_path = file_path + ":Zone.Identifier" + if os.path.exists(blocked_path): + print(f"The file {file_path} is blocked.") + sys.exit(1) + + +def check_file_access(file_path): + try: + # Check if the file exists + if not os.path.exists(file_path): + print(f"The file {file_path} does not exist.") + sys.exit(1) + check_file_blocked(file_path) + + except Exception as e: + print(f"Error checking file access: {e}") + return False + def load_dotnet_framework_assembly(assembly_path, verbose = False): """ Parameters: @@ -267,6 +287,7 @@ def load_dotnet_framework_assembly(assembly_path, verbose = False): Returns: The loaded assembly reference or raises an exception on failure. """ + check_file_access(assembly_path + '.dll') try: import clr if verbose: