├── .gitignore ├── acpc_server ├── dealer ├── dealer.exe ├── msys-2.0.dll ├── protocol.odt ├── protocol.pdf ├── bm_run_matches ├── example_player ├── all_in_expectation ├── bm_run_matches.exe ├── example_player.exe ├── all_in_expectation.exe ├── example_player.nolimit.2p.sh ├── dyypholdem_match.bat ├── dyypholdem_match.sh ├── kuhn.limit.3p.game ├── holdem.nolimit.3p.game ├── holdem.nolimit.2p.reverse_blinds.game ├── holdem.limit.3p.game ├── holdem.limit.2p.reverse_blinds.game ├── README.submission ├── bm_server.config ├── play_match.pl ├── acpc_play_match.pl └── README ├── data └── models │ ├── flop │ ├── final_cpu.tar │ └── final_gpu.tar │ ├── river │ ├── final_cpu.tar │ └── final_gpu.tar │ ├── turn │ ├── final_cpu.tar │ └── final_gpu.tar │ └── preflop-aux │ ├── final_cpu.tar │ └── final_gpu.tar ├── src ├── nn │ ├── bucketing │ │ ├── river_ihr.pkl │ │ ├── flop_dist_cats.pkl │ │ ├── ihr_pair_to_bucket.pkl │ │ ├── preflop_buckets.pt │ │ ├── turn_dist_cats.pkl │ │ ├── bucketing_data.sqlite │ │ ├── flop_tools.py │ │ ├── turn_tools.py │ │ └── river_tools.py │ ├── show_model_info.py │ ├── modules │ │ ├── mul_constant.py │ │ ├── add_table.py │ │ ├── criterion.py │ │ ├── replicate.py │ │ ├── narrow.py │ │ ├── module_factory.py │ │ ├── smooth_loss.py │ │ ├── prelu.py │ │ ├── container.py │ │ ├── dot_product.py │ │ ├── select_table.py │ │ ├── sequential.py │ │ ├── linear.py │ │ ├── masked_huber_loss.py │ │ ├── concat_table.py │ │ ├── utils.py │ │ └── batch_norm.py │ ├── bucket_conversion.py │ ├── optimizer │ │ └── adam.py │ ├── net_builder.py │ └── value_nn.py ├── game │ ├── evaluation │ │ └── hand_ranks.pt │ ├── card_to_string_conversion.py │ └── bet_sizing.py ├── terminal_equity │ ├── block_matrix.pt │ └── preflop_equity.pt ├── tests │ ├── ranges │ │ ├── flop-situation2-p2.txt │ │ ├── situation-p2.txt │ │ ├── situation-p1.txt │ │ ├── situation2-p1.txt │ │ └── situation2-p2.txt │ ├── test_preflop.py │ ├── test_river.py │ ├── test_flop.py │ ├── test_turn.py │ ├── main_test.py │ └── dyypholdem_test.py ├── lookahead │ ├── resolve_results.py │ └── cfrd_gadget.py ├── settings │ ├── game_settings.py │ ├── constants.py │ └── arguments.py ├── data_generation │ ├── random_card_generator.py │ ├── main_data_generation.py │ └── range_generator.py ├── training │ ├── main_train.py │ ├── raw_converter.py │ └── data_stream.py ├── torch7 │ ├── torch7_model_converter.py │ ├── torch7_factory.py │ ├── torch7_serialization.py │ └── torch7_file.py ├── server │ ├── network_communication.py │ └── acpc_game.py ├── tree │ ├── tree_node.py │ └── strategy_filling.py ├── utils │ ├── timer.py │ ├── pseudo_random.py │ └── output.py └── player │ ├── manual_acpc_player.py │ ├── dyypholdem_slumbot_player.py │ └── dyypholdem_acpc_player.py ├── .gitattributes └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | __pycache__/ 3 | *.py[cod] 4 | -------------------------------------------------------------------------------- /acpc_server/dealer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucky72s/dyypholdem/HEAD/acpc_server/dealer -------------------------------------------------------------------------------- /acpc_server/dealer.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucky72s/dyypholdem/HEAD/acpc_server/dealer.exe -------------------------------------------------------------------------------- /acpc_server/msys-2.0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucky72s/dyypholdem/HEAD/acpc_server/msys-2.0.dll -------------------------------------------------------------------------------- /acpc_server/protocol.odt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucky72s/dyypholdem/HEAD/acpc_server/protocol.odt -------------------------------------------------------------------------------- /acpc_server/protocol.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucky72s/dyypholdem/HEAD/acpc_server/protocol.pdf -------------------------------------------------------------------------------- /acpc_server/bm_run_matches: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucky72s/dyypholdem/HEAD/acpc_server/bm_run_matches -------------------------------------------------------------------------------- /acpc_server/example_player: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucky72s/dyypholdem/HEAD/acpc_server/example_player -------------------------------------------------------------------------------- /acpc_server/all_in_expectation: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucky72s/dyypholdem/HEAD/acpc_server/all_in_expectation -------------------------------------------------------------------------------- /acpc_server/bm_run_matches.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucky72s/dyypholdem/HEAD/acpc_server/bm_run_matches.exe -------------------------------------------------------------------------------- /acpc_server/example_player.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucky72s/dyypholdem/HEAD/acpc_server/example_player.exe -------------------------------------------------------------------------------- /acpc_server/all_in_expectation.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucky72s/dyypholdem/HEAD/acpc_server/all_in_expectation.exe -------------------------------------------------------------------------------- /acpc_server/example_player.nolimit.2p.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./example_player holdem.nolimit.2p.reverse_blinds.game $1 $2 3 | -------------------------------------------------------------------------------- /acpc_server/dyypholdem_match.bat: -------------------------------------------------------------------------------- 1 | dealer acpc_match holdem.nolimit.2p.reverse_blinds.game %1 %2 DyypHoldem Hero -p 18901,18902 --t_per_hand 600000 -------------------------------------------------------------------------------- /acpc_server/dyypholdem_match.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./dealer acpc_match holdem.nolimit.2p.reverse_blinds.game $1 $2 DyypHoldem Hero -p 18901,18902 --t_per_hand 600000 4 | -------------------------------------------------------------------------------- /data/models/flop/final_cpu.tar: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:553ee66e14ba09946389e9e6f5c277b4ae3410a2a353d3374c91cad6bf46f3a3 3 | size 192143150 4 | -------------------------------------------------------------------------------- /data/models/flop/final_gpu.tar: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:46455414f33ac083b0950594b85a2cc7ca5fa150e9ce0b63aa9f24e0137284f7 3 | size 200144203 4 | -------------------------------------------------------------------------------- /data/models/river/final_cpu.tar: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c42ac5cad944f7988aba0075e8b892d8032f25c7e583ddf66ded8c2eeebb0944 3 | size 116743214 4 | -------------------------------------------------------------------------------- /data/models/river/final_gpu.tar: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1ff93b13cf06c586e56fe6ee18bdbe0566249d551ac023018e6ed2e9ec17bc8a 3 | size 120776203 4 | -------------------------------------------------------------------------------- /data/models/turn/final_cpu.tar: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:04fc5f3bdfe6fee298a4dbdb0b909e5398c6e7965a9fa130cdadcb5c6529265a 3 | size 192143150 4 | -------------------------------------------------------------------------------- /data/models/turn/final_gpu.tar: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1e946a4bf9f5d6f02739d7fcb4da0fd61ff9b4e4508528b6def4650d6102483e 3 | size 200144139 4 | -------------------------------------------------------------------------------- /src/nn/bucketing/river_ihr.pkl: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:cbe82220f1ea5082e9f3f6daa525c2ac4df89e6043f8928f2eb134859ac50d33 3 | size 188711781 4 | -------------------------------------------------------------------------------- /data/models/preflop-aux/final_cpu.tar: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:21830bd7ed472c314cfcc13ca1d88a9db2f8506ee27d6cf67166887a539f37c8 3 | size 65817902 4 | -------------------------------------------------------------------------------- /data/models/preflop-aux/final_gpu.tar: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:07132c3280af0adfe00b16475804465fe79f4532baa2f97b9473ad905845a5f5 3 | size 67170635 4 | -------------------------------------------------------------------------------- /src/game/evaluation/hand_ranks.pt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f896304f2dde706945978fed38069dfc9a9a06d3f2970afb702f1514f9587a68 3 | size 259903403 4 | -------------------------------------------------------------------------------- /src/nn/bucketing/flop_dist_cats.pkl: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:335dfa94cfe79d77db64ea226493a601ed293c803188f780dc1bd67ebcbf5392 3 | size 10499158 4 | -------------------------------------------------------------------------------- /src/nn/bucketing/ihr_pair_to_bucket.pkl: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:8f6df2a556c25f6e5f59417cc7a99558d4300520278844411523894698c24857 3 | size 3041 4 | -------------------------------------------------------------------------------- /src/nn/bucketing/preflop_buckets.pt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:131814be7cec451cd4cdc894007db16b5c0eb83a9afc6ff7132e361ee2f4a1bc 3 | size 117219115 4 | -------------------------------------------------------------------------------- /src/nn/bucketing/turn_dist_cats.pkl: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4697af9bfc5e17e243557d74092326b669148fb35fd10d151cf130f7493037f7 3 | size 116507098 4 | -------------------------------------------------------------------------------- /src/terminal_equity/block_matrix.pt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d28b9561b182e43dc86901f713d60c7e94cd4a69f76bf5a27c825d5b3333e80d 3 | size 7033835 4 | -------------------------------------------------------------------------------- /src/terminal_equity/preflop_equity.pt: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ad47a518612c5a0c92d44fbef570fb2c005cd96b76536e7ca4d420663cfba7c8 3 | size 7033835 4 | -------------------------------------------------------------------------------- /src/nn/bucketing/bucketing_data.sqlite: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e1b67884f4725c4f481e1f9c6eb23c634b67ab9053369f51a8d49814c64a64d1 3 | size 405417984 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pt filter=lfs diff=lfs merge=lfs -text 2 | *.pkl filter=lfs diff=lfs merge=lfs -text 3 | *.model filter=lfs diff=lfs merge=lfs -text 4 | *.tar filter=lfs diff=lfs merge=lfs -text 5 | *.sqlite filter=lfs diff=lfs merge=lfs -text 6 | -------------------------------------------------------------------------------- /acpc_server/kuhn.limit.3p.game: -------------------------------------------------------------------------------- 1 | GAMEDEF 2 | limit 3 | numPlayers = 3 4 | numRounds = 1 5 | blind = 1 1 1 6 | raiseSize = 1 7 | firstPlayer = 1 8 | maxRaises = 1 9 | numSuits = 1 10 | numRanks = 4 11 | numHoleCards = 1 12 | numBoardCards = 0 13 | END GAMEDEF 14 | -------------------------------------------------------------------------------- /acpc_server/holdem.nolimit.3p.game: -------------------------------------------------------------------------------- 1 | GAMEDEF 2 | nolimit 3 | numPlayers = 3 4 | numRounds = 4 5 | stack = 20000 20000 20000 6 | blind = 50 100 0 7 | firstPlayer = 3 1 1 1 8 | numSuits = 4 9 | numRanks = 13 10 | numHoleCards = 2 11 | numBoardCards = 0 3 1 1 12 | END GAMEDEF 13 | -------------------------------------------------------------------------------- /acpc_server/holdem.nolimit.2p.reverse_blinds.game: -------------------------------------------------------------------------------- 1 | GAMEDEF 2 | nolimit 3 | numPlayers = 2 4 | numRounds = 4 5 | stack = 20000 20000 6 | blind = 100 50 7 | firstPlayer = 2 1 1 1 8 | numSuits = 4 9 | numRanks = 13 10 | numHoleCards = 2 11 | numBoardCards = 0 3 1 1 12 | END GAMEDEF 13 | -------------------------------------------------------------------------------- /acpc_server/holdem.limit.3p.game: -------------------------------------------------------------------------------- 1 | GAMEDEF 2 | limit 3 | numPlayers = 3 4 | numRounds = 4 5 | blind = 5 10 0 6 | raiseSize = 10 10 20 20 7 | firstPlayer = 3 1 1 1 8 | maxRaises = 3 4 4 4 9 | numSuits = 4 10 | numRanks = 13 11 | numHoleCards = 2 12 | numBoardCards = 0 3 1 1 13 | END GAMEDEF 14 | -------------------------------------------------------------------------------- /acpc_server/holdem.limit.2p.reverse_blinds.game: -------------------------------------------------------------------------------- 1 | GAMEDEF 2 | limit 3 | numPlayers = 2 4 | numRounds = 4 5 | blind = 10 5 6 | raiseSize = 10 10 20 20 7 | firstPlayer = 2 1 1 1 8 | maxRaises = 3 4 4 4 9 | numSuits = 4 10 | numRanks = 13 11 | numHoleCards = 2 12 | numBoardCards = 0 3 1 1 13 | END GAMEDEF 14 | -------------------------------------------------------------------------------- /src/tests/ranges/flop-situation2-p2.txt: -------------------------------------------------------------------------------- 1 | 6d7h 0.830509 2 | 6c7h 0.830509 3 | 6c7s 0.830509 4 | 6d7s 0.830509 5 | 6h7c 0.830509 6 | 6h7d 0.830509 7 | 6c7d 0.830509 8 | 6h7s 0.830509 9 | 6s7h 0.830509 10 | 6s7c 0.830509 11 | 6s7d 0.830509 12 | 6d7c 0.830509 13 | 7cJh 0.819686 14 | 7cJd 0.819686 15 | 7dJc 0.819686 16 | 7hJc 0.819686 17 | 7dJh 0.819686 18 | 7dJs 0.819686 19 | 7sJd 0.819686 20 | 7cJs 0.819686 21 | -------------------------------------------------------------------------------- /src/nn/show_model_info.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import argparse 4 | sys.path.append(os.getcwd()) 5 | 6 | 7 | if __name__ == '__main__': 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument("file", help="Model file to show information for") 10 | args = parser.parse_args() 11 | 12 | import settings.arguments as arguments 13 | from nn.value_nn import ValueNn 14 | 15 | nn = ValueNn().load_info_from_file(args.file) 16 | 17 | arguments.logger.info(repr(nn.model)) 18 | arguments.logger.info(repr(nn)) 19 | 20 | arguments.logger.success("Done.") 21 | 22 | -------------------------------------------------------------------------------- /src/lookahead/resolve_results.py: -------------------------------------------------------------------------------- 1 | 2 | import settings.arguments as arguments 3 | 4 | 5 | class ResolveResults(object): 6 | 7 | strategy: arguments.Tensor 8 | achieved_cfvs: arguments.Tensor 9 | root_cfvs: arguments.Tensor 10 | root_cfvs_both_players: arguments.Tensor 11 | children_cfvs: arguments.Tensor 12 | actions: list 13 | 14 | def __init__(self): 15 | pass 16 | 17 | def get_cfv(self, player: int, pocket_index: int) -> float: 18 | return self.root_cfvs_both_players[player, pocket_index].item() 19 | 20 | def get_actions(self): 21 | return self.actions 22 | 23 | def get_player_strategy(self, action_index: int, pocket_index: int) -> float: 24 | return self.strategy[action_index, 0, pocket_index].item() 25 | -------------------------------------------------------------------------------- /src/settings/game_settings.py: -------------------------------------------------------------------------------- 1 | from math import comb 2 | 3 | # definition of a deck of poker cards 4 | suit_count = 4 5 | rank_count = 13 6 | card_count = suit_count * rank_count 7 | 8 | # names for suit and rank of poker card 9 | suit_table = ['c', 'd', 'h', 's'] 10 | rank_table = ['2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K', 'A'] 11 | 12 | # hole cards per hand 13 | hand_card_count = 2 14 | hand_count = comb(card_count, hand_card_count) 15 | 16 | # board structure for texas hold'em 17 | board_card_count = [0, 3, 4, 5] 18 | 19 | # the blind structure, in chips 20 | ante = 100 21 | small_blind = 50 22 | big_blind = 100 23 | # the size of each player's stack, in chips 24 | stack = 20000 25 | # list of pot-scaled bet sizes to use in tree 26 | # orig: bet_sizing = [[1], [1], [1]] 27 | bet_sizing = [[1], [1], [1]] 28 | 29 | -------------------------------------------------------------------------------- /src/nn/modules/mul_constant.py: -------------------------------------------------------------------------------- 1 | 2 | import settings.arguments as arguments 3 | 4 | from nn.modules.module import Module 5 | 6 | 7 | class MulConstant(Module): 8 | 9 | def __init__(self, constant_scalar): 10 | super(MulConstant, self).__init__() 11 | self.constant_scalar = constant_scalar 12 | 13 | def update_output(self, input): 14 | self.output = arguments.Tensor() 15 | self.output.resize_as_(input) 16 | 17 | self.output.copy_(input) 18 | self.output.mul_(self.constant_scalar) 19 | 20 | return self.output 21 | 22 | def update_grad_input(self, input, gradOutput): 23 | if self.gradInput is None: 24 | return 25 | 26 | self.gradInput.resize_as_(gradOutput) 27 | self.gradInput.copy_(gradOutput) 28 | self.gradInput.mul_(self.constant_scalar) 29 | 30 | return self.gradInput 31 | -------------------------------------------------------------------------------- /src/nn/modules/add_table.py: -------------------------------------------------------------------------------- 1 | 2 | import settings.arguments as arguments 3 | 4 | from nn.modules.module import Module 5 | 6 | 7 | class CAddTable(Module): 8 | 9 | def __init__(self): 10 | super(CAddTable, self).__init__() 11 | self.gradInput = [] 12 | 13 | def update_output(self, input): 14 | self.output = arguments.Tensor() 15 | self.output.resize_as_(input[0]).copy_(input[0]) 16 | 17 | for i in range(1, len(input)): 18 | self.output.add_(input[i]) 19 | 20 | return self.output 21 | 22 | def update_grad_input(self, input, gradOutput): 23 | for i in range(len(input)): 24 | if i >= len(self.gradInput): 25 | assert i == len(self.gradInput) 26 | self.gradInput.append(input[0].new()) 27 | self.gradInput[i].resize_as_(input[i]).copy_(gradOutput) 28 | 29 | del self.gradInput[len(input):] 30 | 31 | return self.gradInput 32 | -------------------------------------------------------------------------------- /src/data_generation/random_card_generator.py: -------------------------------------------------------------------------------- 1 | 2 | import torch 3 | 4 | import settings.arguments as arguments 5 | import settings.game_settings as game_settings 6 | 7 | import utils.pseudo_random as random_ 8 | 9 | 10 | # -- Samples a random set of cards. 11 | # -- 12 | # -- Each subset of the deck of the correct size is sampled with 13 | # -- uniform probability. 14 | # -- 15 | # -- @param count the number of cards to sample 16 | # -- @return a vector of cards, represented numerical 17 | def generate_cards(count): 18 | # marking all used cards 19 | used_cards = torch.ByteTensor(game_settings.card_count).zero_() 20 | 21 | out = arguments.Tensor(count) 22 | generated_cards_count = 0 23 | while generated_cards_count < count: 24 | card = random_.randint(0, game_settings.card_count - 1) 25 | if used_cards[card] == 0: 26 | out[generated_cards_count] = card 27 | generated_cards_count += 1 28 | used_cards[card] = 1 29 | return out.int() 30 | 31 | -------------------------------------------------------------------------------- /src/data_generation/main_data_generation.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import argparse 4 | sys.path.append(os.getcwd()) 5 | 6 | 7 | def run(street: int): 8 | if street == 0: 9 | data_generation = DataGeneratorAux() 10 | data_generation.generate_data(arguments.gen_data_count, 1) 11 | else: 12 | data_generation = DataGenerator() 13 | data_generation.generate_data(arguments.gen_data_count, street) 14 | 15 | 16 | if __name__ == '__main__': 17 | parser = argparse.ArgumentParser(description='Generate training data for the specified street') 18 | parser.add_argument('street', type=int, choices=[0, 1, 2, 3, 4], help="Street (0=preflop-aux, 1=pre-flop, 2=flop, 3=turn, 4=river)") 19 | args = parser.parse_args() 20 | 21 | import settings.arguments as arguments 22 | from data_generation import DataGenerator 23 | from aux_data_generation import DataGeneratorAux 24 | 25 | street_arg = int(sys.argv[1]) 26 | arguments.logger.info(f"Generating data for street: {street_arg}") 27 | run(street_arg) 28 | -------------------------------------------------------------------------------- /src/tests/test_preflop.py: -------------------------------------------------------------------------------- 1 | 2 | import settings.arguments as arguments 3 | import settings.constants as constants 4 | import settings.game_settings as game_settings 5 | 6 | import game.card_tools as card_tools 7 | import game.card_to_string_conversion as card_to_string 8 | from tree.tree_node import TreeNode 9 | 10 | 11 | def prepare_test(): 12 | 13 | current_node = TreeNode() 14 | 15 | current_node.board = card_to_string.string_to_board('') 16 | current_node.street = 1 17 | current_node.current_player = constants.Players.P1 18 | current_node.bets = arguments.Tensor([50, 100]) 19 | current_node.num_bets = 1 20 | 21 | player_range_tensor = arguments.Tensor(1, game_settings.hand_count) 22 | opponent_range_tensor = arguments.Tensor(1, game_settings.hand_count) 23 | 24 | # uniform range 25 | player_range_tensor[0].copy_(card_tools.get_uniform_range(current_node.board)) 26 | opponent_range_tensor[0].copy_(card_tools.get_uniform_range(current_node.board)) 27 | 28 | return current_node, player_range_tensor, opponent_range_tensor 29 | 30 | -------------------------------------------------------------------------------- /src/nn/modules/criterion.py: -------------------------------------------------------------------------------- 1 | 2 | import settings.arguments as arguments 3 | 4 | from nn.modules.utils import recursive_type 5 | 6 | 7 | class Criterion(object): 8 | 9 | def __init__(self): 10 | self.gradInput = arguments.Tensor() 11 | self.output = 0 12 | 13 | def forward(self, input, target): 14 | return self.update_output(input, target) 15 | 16 | def update_output(self, input, target): 17 | raise NotImplementedError 18 | 19 | def backward(self, input, target): 20 | return self.update_grad_input(input, target) 21 | 22 | def update_grad_input(self, input, target): 23 | raise NotImplementedError 24 | 25 | def type(self, type, tensorCache=None): 26 | # find all tensors and convert them 27 | for key, param in self.__dict__.items(): 28 | setattr(self, key, recursive_type(param, type, tensorCache or {})) 29 | return self 30 | 31 | def float(self): 32 | return self.type('torch.FloatTensor') 33 | 34 | def double(self): 35 | return self.type('torch.DoubleTensor') 36 | 37 | def cuda(self): 38 | return self.type('torch.cuda.FloatTensor') 39 | -------------------------------------------------------------------------------- /src/nn/modules/replicate.py: -------------------------------------------------------------------------------- 1 | 2 | import torch 3 | 4 | from nn.modules.module import Module 5 | 6 | 7 | class Replicate(Module): 8 | 9 | def __init__(self, nf, dim=0, ndim=0): 10 | super(Replicate, self).__init__() 11 | self.nfeatures = nf 12 | self.dim = dim 13 | self.ndim = ndim 14 | assert self.dim >= 0 15 | 16 | def update_output(self, input): 17 | assert self.dim <= input.dim() 18 | 19 | size = [int(x) for x in input.size()] # convert sizes to int 20 | 21 | size.insert(int(self.dim), int(self.nfeatures)) 22 | 23 | stride = list(input.stride()) 24 | stride.insert(self.dim, 0) 25 | 26 | self.output.set_(input.storage(), input.storage_offset(), 27 | torch.Size(size), tuple(stride)) 28 | return self.output 29 | 30 | def update_grad_input(self, input, gradOutput): 31 | self.gradInput.resize_as_(input).zero_() 32 | size = list(input.size()) 33 | size.insert(self.dim, 1) 34 | 35 | grad_input = self.gradInput.view(*size) 36 | torch.sum(gradOutput, self.dim, True, out=grad_input) 37 | return self.gradInput 38 | -------------------------------------------------------------------------------- /src/nn/modules/narrow.py: -------------------------------------------------------------------------------- 1 | 2 | import settings.arguments as arguments 3 | 4 | from nn.modules.module import Module 5 | 6 | 7 | class Narrow(Module): 8 | 9 | def __init__(self, dimension, offset, length=1): 10 | super(Narrow, self).__init__() 11 | self.dimension = dimension 12 | self.index = offset 13 | self.length = length 14 | 15 | def update_output(self, input): 16 | length = self.length 17 | if length < 0: 18 | length = input.size(self.dimension) - self.index + self.length + 1 19 | 20 | output = input.narrow(self.dimension, self.index, length) 21 | 22 | self.output = arguments.Tensor() 23 | self.output.resize_as_(output).copy_(output) 24 | 25 | return self.output 26 | 27 | def update_grad_input(self, input, gradOutput): 28 | length = self.length 29 | if length < 0: 30 | length = input.size(self.dimension) - self.index + self.length + 1 31 | 32 | self.gradInput = self.gradInput.type_as(input) 33 | self.gradInput.resize_as_(input).zero_() 34 | self.gradInput.narrow(self.dimension, self.index, length).copy_(gradOutput) 35 | return self.gradInput 36 | -------------------------------------------------------------------------------- /src/nn/modules/module_factory.py: -------------------------------------------------------------------------------- 1 | 2 | from nn.modules.sequential import Sequential 3 | from nn.modules.concat_table import ConcatTable 4 | from nn.modules.select_table import SelectTable 5 | from nn.modules.add_table import CAddTable 6 | from nn.modules.linear import Linear 7 | from nn.modules.prelu import PReLU 8 | from nn.modules.batch_norm import BatchNormalization 9 | from nn.modules.narrow import Narrow 10 | from nn.modules.dot_product import DotProduct 11 | from nn.modules.replicate import Replicate 12 | from nn.modules.mul_constant import MulConstant 13 | 14 | 15 | class ModuleFactory(object): 16 | 17 | module_types = { 18 | "nn.Sequential": Sequential, 19 | "nn.ConcatTable": ConcatTable, 20 | "nn.SelectTable": SelectTable, 21 | "nn.CAddTable": CAddTable, 22 | "nn.Linear": Linear, 23 | "nn.PReLU": PReLU, 24 | "nn.BatchNormalization": BatchNormalization, 25 | "nn.Narrow": Narrow, 26 | "nn.DotProduct": DotProduct, 27 | "nn.Replicate": Replicate, 28 | "nn.MulConstant": MulConstant, 29 | } 30 | 31 | def create_module(self, module) -> object: 32 | return self.module_types[module].__new__(self.module_types[module]) 33 | -------------------------------------------------------------------------------- /src/nn/modules/smooth_loss.py: -------------------------------------------------------------------------------- 1 | 2 | import torch 3 | import torch.nn.functional as F 4 | 5 | from nn.modules.criterion import Criterion 6 | 7 | 8 | class SmoothL1Criterion(Criterion): 9 | 10 | def __init__(self, size_average=True, reduce=None, reduction: str = 'mean', beta: float = 1.0): 11 | super(SmoothL1Criterion, self).__init__() 12 | self.sizeAverage = size_average 13 | self.output_tensor = None 14 | self.reduction = reduction 15 | self.beta = beta 16 | 17 | def update_output(self, input, target): 18 | if self.output_tensor is None: 19 | self.output_tensor = input.new(1) 20 | self.output_tensor = F.smooth_l1_loss(input, target, reduction=self.reduction, beta=self.beta) 21 | self.output = self.output_tensor.item() 22 | return self.output 23 | 24 | def update_grad_input(self, input, target): 25 | self.gradInput = input.new(input.size()) 26 | norm = 1.0 / input.numel() if self.sizeAverage else 1.0 27 | 28 | delta = input.data - target.data 29 | self.gradInput.copy_(delta) 30 | self.gradInput *= norm 31 | self.gradInput[torch.lt(delta, -1.0)] = -norm 32 | self.gradInput[torch.gt(delta, 1.0)] = norm 33 | 34 | return self.gradInput 35 | -------------------------------------------------------------------------------- /src/tests/test_river.py: -------------------------------------------------------------------------------- 1 | 2 | import settings.arguments as arguments 3 | import settings.constants as constants 4 | 5 | import game.card_tools as card_tools 6 | import game.card_to_string_conversion as card_to_string 7 | from tree.tree_node import TreeNode 8 | 9 | 10 | def prepare_test(): 11 | 12 | current_node = TreeNode() 13 | 14 | current_node.board = card_to_string.string_to_board('7d7c8s5sQd') 15 | current_node.street = 4 16 | current_node.current_player = constants.Players.P2 17 | current_node.bets = arguments.Tensor([8000, 8000]) 18 | current_node.num_bets = 0 19 | 20 | arguments.logger.debug( 21 | f"Board: {card_to_string.cards_to_string(current_node.board)}, Bets: {current_node.bets[0]}, {current_node.bets[1]}") 22 | 23 | player_range = card_tools.get_file_range('tests/ranges/situation-p2.txt') 24 | opponent_range = card_tools.get_file_range('tests/ranges/situation-p1.txt') 25 | 26 | player_range_tensor = arguments.Tensor(1, player_range.size(0)) 27 | opponent_range_tensor = arguments.Tensor(1, opponent_range.size(0)) 28 | 29 | # ranges from file 30 | player_range_tensor[0].copy_(player_range) 31 | opponent_range_tensor[0].copy_(opponent_range) 32 | 33 | return current_node, player_range_tensor, opponent_range_tensor 34 | -------------------------------------------------------------------------------- /src/tests/test_flop.py: -------------------------------------------------------------------------------- 1 | 2 | import settings.arguments as arguments 3 | import settings.constants as constants 4 | 5 | import game.card_tools as card_tools 6 | import game.card_to_string_conversion as card_to_string 7 | from tree.tree_node import TreeNode 8 | 9 | 10 | def prepare_test(): 11 | 12 | current_node = TreeNode() 13 | 14 | current_node.board = card_to_string.string_to_board('3cAdKc') 15 | current_node.street = 2 16 | current_node.current_player = constants.Players.P2 17 | current_node.bets = arguments.Tensor([600, 600]) 18 | current_node.num_bets = 0 19 | 20 | player_range = card_tools.get_file_range('tests/ranges/flop-situation3-p2.txt') 21 | opponent_range = card_tools.get_file_range('tests/ranges/flop-situation3-p1.txt') 22 | 23 | player_range_tensor = arguments.Tensor(1, player_range.size(0)) 24 | opponent_range_tensor = arguments.Tensor(1, opponent_range.size(0)) 25 | 26 | # ranges from file 27 | player_range_tensor[0].copy_(player_range) 28 | opponent_range_tensor[0].copy_(opponent_range) 29 | 30 | # random ranges 31 | # player_range_tensor[0].copy_(card_tools.get_uniform_range(current_node.board)) 32 | # opponent_range_tensor[0].copy_(card_tools.get_uniform_range(current_node.board)) 33 | 34 | return current_node, player_range_tensor, opponent_range_tensor 35 | -------------------------------------------------------------------------------- /src/nn/modules/prelu.py: -------------------------------------------------------------------------------- 1 | 2 | import torch 3 | 4 | import settings.arguments as arguments 5 | 6 | from nn.modules.module import Module 7 | from nn.modules.utils import clear 8 | 9 | 10 | class PReLU(Module): 11 | 12 | def __init__(self, nOutputPlane=0): 13 | super(PReLU, self).__init__() 14 | # if no argument provided, use shared model (weight is scalar) 15 | self.nOutputPlane = nOutputPlane 16 | self.weight = arguments.Tensor(nOutputPlane or 1).fill_(0.25) 17 | self.gradWeight = arguments.Tensor(nOutputPlane or 1) 18 | 19 | def update_output(self, input): 20 | self.output.resize_as_(input) 21 | self.output.copy_(input) 22 | self.output[torch.le(input, 0.0)] *= self.weight 23 | return self.output 24 | 25 | def update_grad_input(self, input, gradOutput): 26 | self.gradInput.resize_as_(gradOutput) 27 | self.gradInput.copy_(gradOutput) 28 | self.gradInput[torch.le(input, 0.0)] *= self.weight 29 | return self.gradInput 30 | 31 | def acc_grad_parameters(self, input, gradOutput, scale=1): 32 | idx = torch.le(input, 0) 33 | _sum = torch.sum(input[idx] * gradOutput[idx]) 34 | self.gradWeight += scale * _sum 35 | return self.gradWeight 36 | 37 | def clear_state(self): 38 | clear(self, 'gradWeightBuf', 'gradWeightBuf2') 39 | return super(PReLU, self).clear_state() 40 | -------------------------------------------------------------------------------- /src/training/main_train.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import argparse 4 | sys.path.append(os.getcwd()) 5 | 6 | 7 | def train(street: int): 8 | 9 | start_epoch = 0 10 | state = None 11 | if arguments.resume_training: 12 | # reload existing network 13 | nn = ValueNn().load_for_street(street - 1, training=True) 14 | start_epoch = nn.model_info["epoch"] 15 | model = nn.model 16 | state = nn.model_state 17 | else: 18 | # create empty neural network 19 | model = TrainingNetwork().build_net(street) 20 | 21 | data_stream = DataStream(street) 22 | 23 | training.train(street, model, state, data_stream, start_epoch, start_epoch + arguments.epoch_count) 24 | 25 | 26 | if __name__ == '__main__': 27 | parser = argparse.ArgumentParser(description='Train a neural network for the specified street') 28 | parser.add_argument('street', type=int, choices=[1, 2, 3, 4], help="Street (1=pre-flop, 2=flop, 3=turn, 4=river)") 29 | args = parser.parse_args() 30 | 31 | import settings.arguments as arguments 32 | 33 | from nn.net_builder import TrainingNetwork 34 | from training.data_stream import DataStream 35 | import training.train as training 36 | from nn.value_nn import ValueNn 37 | 38 | street_arg = int(sys.argv[1]) 39 | 40 | arguments.logger.info(f"Training model for street: {street_arg}") 41 | 42 | train(street_arg) 43 | -------------------------------------------------------------------------------- /src/torch7/torch7_model_converter.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import argparse 4 | sys.path.append(os.getcwd()) 5 | 6 | 7 | def convert_t7_to_pytorch(file_name: str, street: int, mode="ascii"): 8 | 9 | torch7_info = torch7_file.read_model_from_torch7_file(file_name.replace(".model", ".info"), mode) 10 | torch7_model = torch7_file.read_model_from_torch7_file(file_name, mode) 11 | if arguments.use_gpu: 12 | torch7_model.cuda() 13 | 14 | model_file_name = file_name.replace(".model", ".tar") 15 | ValueNn().save_model(torch7_model, model_file_name, street=street, epoch=int(torch7_info['epoch']), valid_loss=torch7_info['valid_loss']) 16 | 17 | 18 | if __name__ == '__main__': 19 | parser = argparse.ArgumentParser() 20 | parser.add_argument("file", help="torch7 model file to convert") 21 | parser.add_argument("street", type=int, choices=[1, 2, 3, 4], help="the street of the model (1=pre-flop, 2=flop, 3=turn, 4=river)") 22 | parser.add_argument("mode", type=str, choices=['binary', 'ascii'], help="file mode of the model ('binary' or 'ascii'") 23 | args = parser.parse_args() 24 | 25 | import settings.arguments as arguments 26 | 27 | from nn.value_nn import ValueNn 28 | import torch7_file as torch7_file 29 | 30 | file_mode = "rb" 31 | if args.mode == "ascii": 32 | file_mode = "r" 33 | 34 | arguments.logger.info(f"Converting file '{args.file}' with type '{args.mode}' for target device '{arguments.device}'") 35 | convert_t7_to_pytorch(args.file, args.street, file_mode) 36 | arguments.logger.success("Conversion completed") 37 | -------------------------------------------------------------------------------- /src/server/network_communication.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | 4 | class ACPCNetworkCommunication(object): 5 | 6 | connection: socket 7 | 8 | def __init__(self): 9 | pass 10 | 11 | # --- Connects over a network socket. 12 | # -- 13 | # -- @param server the server that sends states to DyypHoldem, and to which 14 | # -- DyypHoldem sends actions 15 | # -- @param port the port to connect on 16 | def connect(self, server, port): 17 | self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 18 | self.connection.connect((server, int(port))) 19 | self._handshake() 20 | 21 | # --- Sends a handshake message to initialize network communication. 22 | # -- @local 23 | def _handshake(self): 24 | self.send_line('VERSION:2.0.0') 25 | 26 | # --- Sends a message to the server. 27 | # -- @param line a string to send to the server 28 | def send_line(self, line): 29 | self.connection.send((line + "\r\n").encode()) 30 | 31 | # --- Waits for a text message from the server. Blocks until a message is 32 | # -- received. 33 | # -- @return the message received 34 | def get_line(self, size=1024): 35 | # peek into receive buffer and only receive one line at a time 36 | line = self.connection.recv(size, socket.MSG_PEEK) 37 | eol = line.find(b'\n') 38 | if eol >= 0: 39 | size = eol + 1 40 | else: 41 | size = len(line) 42 | out = self.connection.recv(size).decode() 43 | return out 44 | 45 | # --- Ends the network communication. 46 | def close(self): 47 | self.connection.close() 48 | -------------------------------------------------------------------------------- /src/tree/tree_node.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | import torch 4 | 5 | import settings.arguments as arguments 6 | import settings.constants as constants 7 | 8 | from game.bet_sizing import BetSizing 9 | 10 | 11 | @dataclass 12 | class TreeNode: 13 | depth: int = 0 14 | street: int = 0 15 | board: arguments.Tensor = None 16 | board_string: str = "" 17 | current_player: constants.Players = 0 18 | bets: arguments.Tensor = None 19 | num_bets: int = 0 20 | terminal: bool = False 21 | type: constants.NodeTypes = constants.NodeTypes.undefined 22 | parent: object = None 23 | children: [] = None 24 | actions: arguments.Tensor = None 25 | strategy: arguments.Tensor = None 26 | bet_sizing: BetSizing = None 27 | pot: torch.Tensor = None 28 | lookahead_coordinates: arguments.Tensor = None 29 | 30 | def __repr__(self, level=0): 31 | if level > 3: 32 | return '' 33 | if level == 0: 34 | header = "Decision Tree:\n" 35 | indent = " |> " 36 | else: 37 | header = '' 38 | indent = " " + " " * (level - 1) + "|---> " 39 | ret = f"{header}{indent}Type={self.type}, depth={self.depth}, street={arguments.street_names[self.street]}, player={repr(self.current_player)}, bets=({self.bets[0].item()}, {self.bets[1].item()}), pot={self.pot}\n" 40 | if self.children: 41 | for child in self.children: 42 | ret += child.__repr__(level + 1) 43 | return ret 44 | 45 | 46 | @dataclass 47 | class BuildTreeParams: 48 | root_node: TreeNode 49 | limit_to_street: bool 50 | bet_sizing: BetSizing = None 51 | 52 | -------------------------------------------------------------------------------- /src/tests/test_turn.py: -------------------------------------------------------------------------------- 1 | 2 | import settings.arguments as arguments 3 | import settings.constants as constants 4 | 5 | import game.card_tools as card_tools 6 | import game.card_to_string_conversion as card_to_string 7 | from tree.tree_node import TreeNode 8 | 9 | 10 | def prepare_test(): 11 | 12 | current_node = TreeNode() 13 | 14 | current_node.board = card_to_string.string_to_board('3c5h4h3h') 15 | current_node.street = 3 16 | current_node.current_player = constants.Players.P2 17 | current_node.bets = arguments.Tensor([600, 600]) 18 | current_node.num_bets = 0 19 | 20 | # current_node.board = card_to_string.string_to_board('Ts8c6hAs') 21 | # current_node.street = 3 22 | # current_node.current_player = constants.Players.P2 23 | # current_node.bets = arguments.Tensor([2700, 900]) 24 | # current_node.num_bets = 1 25 | 26 | arguments.logger.debug(f"Board={card_to_string.cards_to_string(current_node.board)}, bets=({current_node.bets[0]}, {current_node.bets[1]})") 27 | 28 | player_range = card_tools.get_file_range('tests/ranges/situation3-p2.txt') 29 | opponent_range = card_tools.get_file_range('tests/ranges/situation3-p1.txt') 30 | 31 | player_range_tensor = arguments.Tensor(1, player_range.size(0)) 32 | opponent_range_tensor = arguments.Tensor(1, opponent_range.size(0)) 33 | 34 | # ranges from file 35 | player_range_tensor[0].copy_(player_range) 36 | opponent_range_tensor[0].copy_(opponent_range) 37 | 38 | # random/uniform ranges 39 | # player_range_tensor[0].copy_(card_tools.get_uniform_range(current_node.board)) 40 | # opponent_range_tensor[0].copy_(card_tools.get_uniform_range(current_node.board)) 41 | 42 | return current_node, player_range_tensor, opponent_range_tensor 43 | -------------------------------------------------------------------------------- /src/settings/constants.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | # --- the number of players in the game 4 | players_count = 2 5 | # --- the number of betting rounds in the game 6 | streets_count = 4 7 | 8 | 9 | # --- IDs for each player and chance 10 | # -- @field chance `-1` 11 | # -- @field P1 `0` 12 | # -- @field P2 `1` 13 | class Players(Enum): 14 | Chance = -1 15 | P1 = 0 16 | P2 = 1 17 | 18 | def __repr__(self): 19 | if self is Players.P1: 20 | return "P1 (SB)" 21 | else: 22 | return "P2 (BB)" 23 | 24 | 25 | # --- IDs for terminal nodes (either after a fold or call action) and nodes that follow a check action 26 | # -- @field terminal_fold (terminal node following fold) `-2` 27 | # -- @field terminal_call (terminal node following call) `-1` 28 | # -- @field chance_node (node for the chance player) `0` 29 | # -- @field check (node following check) `-1` 30 | # -- @field inner_node (any other node) `1` 31 | class NodeTypes(Enum): 32 | terminal_fold = -2 33 | terminal_call = -1 34 | inner_check = -1 35 | chance_node = 0 36 | inner_raise = 1 37 | undefined = -9 38 | 39 | 40 | # --- IDs for fold and check/call actions 41 | # -- @field fold `-2` 42 | # -- @field ccall (check/call) `-1` 43 | class Actions(Enum): 44 | fold = -2 45 | ccall = -1 46 | rraise = -3 47 | 48 | 49 | # --- String representations for actions in the ACPC protocol 50 | # -- @field fold "`fold`" 51 | # -- @field ccall (check/call) "`ccall`" 52 | # -- @field raise "`raise`" 53 | class ACPCActions(Enum): 54 | fold = "fold" 55 | ccall = "ccall" 56 | rraise = "raise" 57 | 58 | 59 | # --- An arbitrarily large number used for clamping regrets. 60 | # --@return the number 61 | def max_number(): 62 | return 999999 63 | -------------------------------------------------------------------------------- /src/nn/modules/container.py: -------------------------------------------------------------------------------- 1 | 2 | from nn.modules.module import Module 3 | from nn.modules.utils import clear 4 | 5 | 6 | class Container(Module): 7 | 8 | def __init__(self, *args): 9 | super(Container, self).__init__() 10 | self.modules = [] 11 | 12 | def add(self, module): 13 | self.modules.append(module) 14 | return self 15 | 16 | def get(self, index): 17 | return self.modules[index] 18 | 19 | def size(self): 20 | return len(self.modules) 21 | 22 | def apply_to_modules(self, func): 23 | for module in self.modules: 24 | func(module) 25 | 26 | def zero_grad_parameters(self): 27 | self.apply_to_modules(lambda m: m.zeroGradParameters()) 28 | 29 | def update_parameters(self, learning_rate): 30 | self.apply_to_modules(lambda m: m.update_parameters(learning_rate)) 31 | 32 | def training(self): 33 | self.apply_to_modules(lambda m: m.training()) 34 | super(Container, self).training() 35 | 36 | def evaluate(self, ): 37 | self.apply_to_modules(lambda m: m.evaluate()) 38 | super(Container, self).evaluate() 39 | 40 | def reset(self, stdv=None): 41 | self.apply_to_modules(lambda m: m.reset(stdv)) 42 | 43 | def parameters(self): 44 | w = [] 45 | gw = [] 46 | for module in self.modules: 47 | mparam = module.parameters() 48 | if mparam is not None: 49 | w.extend(mparam[0]) 50 | gw.extend(mparam[1]) 51 | if not w: 52 | return 53 | return w, gw 54 | 55 | def clear_state(self): 56 | clear('output') 57 | clear('gradInput') 58 | for module in self.modules: 59 | module.clear_state() 60 | return self 61 | -------------------------------------------------------------------------------- /acpc_server/README.submission: -------------------------------------------------------------------------------- 1 | ############################################################# 2 | # Please fill out the following information about your team 3 | ############################################################# 4 | 5 | Team Name: 6 | Agent Name (can be the same as team name): 7 | 8 | For each team member, please list the following: 9 | Name, Team leader (y/n)?, e-mail, Academic (y/n, position - e.g., PhD student)?, University/Business affiliation, Location (city, province/state, country) 10 | 11 | Was this submission part of an academic class project? What level of class 12 | (undergraduate/graduate)? 13 | 14 | 15 | ########################################################################### 16 | # Please provide as much information about your agent as possible as the 17 | # competition organizers are very interested in knowing more about the 18 | # techniques used by our competitors. 19 | ########################################################################### 20 | 21 | 1) Is your agent dynamic? That is, does its strategy change throughout the 22 | course of a match, or is the strategy played the same throughout the match? 23 | 24 | 2) Does your agent use a (approximate) Nash equilibrium strategy? 25 | 26 | 3) Does your agent attempt to model your opponents? If so, does it do so 27 | online during the competition or offline from data (e.g., using logs of play or 28 | the benchmark server)? 29 | 30 | 4) Does your agent use techniques that would benefit from additional CPU time 31 | during the competition? 32 | 33 | 5) Does your agent use techniques that would benefit from additional RAM during 34 | the competition? 35 | 36 | 6) Would you agent benefit from additional disk space? 37 | 38 | One/Two Paragraph Summary of Technique 39 | 40 | 41 | References to relevant papers, if any 42 | 43 | -------------------------------------------------------------------------------- /src/nn/modules/dot_product.py: -------------------------------------------------------------------------------- 1 | 2 | import torch 3 | 4 | import settings.arguments as arguments 5 | 6 | from nn.modules.module import Module 7 | from nn.modules.utils import clear 8 | 9 | 10 | class DotProduct(Module): 11 | 12 | def __init__(self): 13 | super(DotProduct, self).__init__() 14 | self.gradInput = [arguments.Tensor(), arguments.Tensor()] 15 | self.buffer: torch.Tensor = None 16 | 17 | def update_output(self, input): 18 | input1, input2 = input[0], input[1] 19 | 20 | if self.buffer is None: 21 | self.buffer = input1.new() 22 | 23 | self.buffer = arguments.Tensor() 24 | self.buffer.resize_as_(input1) 25 | 26 | torch.mul(input1, input2, out=self.buffer) 27 | 28 | self.output = arguments.Tensor() 29 | self.output.resize_(self.buffer.size(0), 1) 30 | 31 | torch.sum(self.buffer, 1, True, out=self.output) 32 | self.output.resize_(input1.size(0)) 33 | return self.output 34 | 35 | def update_grad_input(self, input, gradOutput): 36 | v1 = input[0] 37 | v2 = input[1] 38 | 39 | if len(self.gradInput) != 2: 40 | if self.gradInput[0] is None: 41 | self.gradInput[0] = input[0].new() 42 | if self.gradInput[1] is None: 43 | self.gradInput[1] = input[1].new() 44 | self.gradInput = self.gradInput[:2] 45 | 46 | gw1 = self.gradInput[0] 47 | gw2 = self.gradInput[1] 48 | gw1.resize_as_(v1).copy_(v2) 49 | gw2.resize_as_(v2).copy_(v1) 50 | 51 | go = gradOutput.contiguous().view(-1, 1).expand_as(v1) 52 | gw1.mul_(go) 53 | gw2.mul_(go) 54 | 55 | return self.gradInput 56 | 57 | def clear_state(self): 58 | clear(self, 'buffer') 59 | return super(DotProduct, self).clear_state() 60 | -------------------------------------------------------------------------------- /src/tests/main_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import argparse 4 | sys.path.append(os.getcwd()) 5 | 6 | 7 | def run_test(node, player_range, opponent_range): 8 | te = TerminalEquity() 9 | te.set_board(current_node.board) 10 | resolving = Resolving(te) 11 | # calculate results 12 | return resolving.resolve_first_node(node, player_range, opponent_range) 13 | 14 | 15 | if __name__ == '__main__': 16 | parser = argparse.ArgumentParser(description='Run test of a specific poker hand on the defined street') 17 | parser.add_argument('street', type=int, choices=[1, 2, 3, 4], help="Street (1=pre-flop, 2=flop, 3=turn, 4=river)") 18 | args = parser.parse_args() 19 | 20 | import settings.arguments as arguments 21 | import test_river 22 | import test_turn 23 | import test_flop 24 | import test_preflop 25 | 26 | from terminal_equity.terminal_equity import TerminalEquity 27 | from lookahead.resolving import Resolving 28 | import utils.output as output 29 | 30 | street = args.street 31 | arguments.logger.info(f"Running test for street: {arguments.street_names[street]}") 32 | prepare_test = None 33 | if street == 4: 34 | prepare_test = test_river.prepare_test 35 | elif street == 3: 36 | prepare_test = test_turn.prepare_test 37 | elif street == 2: 38 | prepare_test = test_flop.prepare_test 39 | elif street == 1: 40 | prepare_test = test_preflop.prepare_test 41 | 42 | arguments.timer.start() 43 | 44 | # prepare test 45 | current_node, player_range_tensor, opponent_range_tensor = prepare_test() 46 | # calculate results 47 | results = run_test(current_node, player_range_tensor, opponent_range_tensor) 48 | # output results 49 | output.show_results(player_range_tensor, current_node, results) 50 | 51 | arguments.timer.stop("Testing completed in: ", log_level="TIMING") 52 | arguments.logger.success("Test completed.") 53 | -------------------------------------------------------------------------------- /src/utils/timer.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | class Timer(object): 5 | 6 | def __init__(self, logger): 7 | self._start: datetime.datetime = None 8 | self._stop: datetime.datetime = None 9 | self._runtime: datetime.datetime = None 10 | self._split_start: datetime.datetime = None 11 | self._split_stop: datetime.datetime = None 12 | self._split_time: datetime.datetime = None 13 | self._elapsed: datetime.datetime = None 14 | self.logger = logger 15 | 16 | def start(self, message=None, log_level="TIMING"): 17 | if message is not None: 18 | self.logger.log(log_level, message) 19 | self._start = datetime.datetime.now() 20 | return self 21 | 22 | def stop(self, message=None, log_level="TIMING"): 23 | self._stop = datetime.datetime.now() 24 | self._runtime = self._stop - self._start 25 | if message is not None: 26 | self.logger.log(log_level, f"{message}: {self._runtime}") 27 | return self._runtime 28 | 29 | def time(self, message="Elapsed: "): 30 | self._elapsed = datetime.datetime.now() - self._start 31 | return message + str(self._elapsed) 32 | 33 | def split_start(self, message=None, log_level="TIMING"): 34 | self._split_start = datetime.datetime.now() 35 | if message is not None: 36 | self.logger.log(log_level, f"{message}...") 37 | return self._split_start 38 | 39 | def split_stop(self, message=None, log_level="TIMING"): 40 | self._split_stop = datetime.datetime.now() 41 | self._split_time = self._split_stop - self._split_start 42 | if message is not None: 43 | self.logger.log(log_level, f"{message}: {self._split_time}") 44 | return self._split_time 45 | 46 | def split_time(self, message="Elapsed in split: "): 47 | self._elapsed = datetime.datetime.now() - self._split_start 48 | return message + str(self._elapsed) 49 | -------------------------------------------------------------------------------- /src/nn/modules/select_table.py: -------------------------------------------------------------------------------- 1 | 2 | from nn.modules.module import Module 3 | from nn.modules.utils import clear, recursive_copy 4 | 5 | 6 | class SelectTable(Module): 7 | 8 | def __init__(self, index): 9 | super(SelectTable, self).__init__() 10 | self.index = index 11 | self.gradInput = [] 12 | 13 | def update_output(self, input): 14 | # handle negative indices 15 | index = self.index if self.index >= 0 else input.size(self.dimension) + self.index 16 | assert len(input) > index 17 | self.output = input[index] 18 | return self.output 19 | 20 | def update_grad_input(self, input, gradOutput): 21 | # make gradInput a zeroed copy of input 22 | self._zero_table_copy(self.gradInput, input) 23 | # handle negative indices 24 | index = self.index if self.index >= 0 else input.size(self.dimension) + self.index 25 | # copy into gradInput[index] (necessary for variable sized inputs) 26 | assert self.gradInput[index] is not None 27 | recursive_copy(self.gradInput[index], gradOutput) 28 | return self.gradInput 29 | 30 | def _zero_table_copy(self, l1, l2): 31 | for i, v in enumerate(l2): 32 | if isinstance(v, list): 33 | if len(l1) > i: 34 | l1[i] = self._zero_table_copy(l1[i], l2[i]) 35 | else: 36 | l1.append(self._zero_table_copy([], l2[i])) 37 | else: 38 | if i >= len(l1): 39 | l1.append(v.new().resize_as_(v).zero_()) 40 | else: 41 | l1[i].resize_as_(v) 42 | l1[i].zero_() 43 | del l1[len(l2):] 44 | return l1 45 | 46 | def type(self, type=None, tensorCache=None): 47 | del self.gradInput[:] 48 | if isinstance(self.output, list): 49 | del self.output[:] 50 | return super(SelectTable, self).type(type, tensorCache) 51 | 52 | def clear_state(self): 53 | clear(self, 'gradInput') 54 | 55 | def __repr__(self): 56 | return super(SelectTable, self).__repr__() + '({})'.format(self.index) 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/tree/strategy_filling.py: -------------------------------------------------------------------------------- 1 | 2 | import settings.arguments as arguments 3 | import settings.constants as constants 4 | import settings.game_settings as game_settings 5 | 6 | import game.card_tools as card_tools 7 | 8 | 9 | class StrategyFilling(object): 10 | 11 | def __init__(self): 12 | pass 13 | 14 | # --- Fills a public tree with a uniform strategy. 15 | # -- @param tree a public tree for Leduc Hold'em or variant 16 | def fill_uniform(self, tree): 17 | self._fill_uniform_dfs(tree) 18 | 19 | # --- Fills a node with a uniform strategy and recurses on the children. 20 | # -- @param node the node 21 | # -- @local 22 | def _fill_uniform_dfs(self, node): 23 | if node.current_player == constants.Players.Chance: 24 | self._fill_chance(node) 25 | else: 26 | self._fill_uniformly(node) 27 | 28 | for i in range(0, len(node.children)): 29 | self._fill_uniform_dfs(node.children[i]) 30 | 31 | # --- Fills a chance node with the probability of each outcome. 32 | # -- @param node the chance node 33 | # -- @local 34 | @staticmethod 35 | def _fill_chance(node): 36 | assert not node.terminal 37 | 38 | node.strategy = arguments.Tensor(len(node.children), game_settings.hand_count).fill_(0) 39 | # setting probability of impossible hands to 0 40 | for i in range(0, len(node.children)): 41 | child_node = node.children[i] 42 | mask = card_tools.get_possible_hand_indexes(child_node.board).byte() 43 | node.strategy[i].fill_(0) 44 | # remove 4 as in Hold'em each player holds one card 45 | node.strategy[i][mask] = 1.0 / (game_settings.card_count - 4) 46 | 47 | # --- Fills a player node with a uniform strategy. 48 | # -- @param node the player node 49 | # -- @local 50 | @staticmethod 51 | def _fill_uniformly(node): 52 | assert node.current_player == constants.Players.P1 or node.current_player == constants.Players.P2 53 | 54 | if not node.terminal: 55 | node.strategy = arguments.Tensor(len(node.children), game_settings.hand_count).fill_(1.0 / len(node.children)) 56 | -------------------------------------------------------------------------------- /src/tests/ranges/situation-p2.txt: -------------------------------------------------------------------------------- 1 | 5d5c 1 2 | 5h5d 1 3 | 5h5c 1 4 | 7sas 0.991 5 | 7hah 0.982 6 | 8d8c 0.969 7 | 8h8c 0.953 8 | 8h8d 0.953 9 | 5h7h 0.870 10 | 7sks 0.855 11 | 7h9h 0.834 12 | 7s9s 0.834 13 | 7hkh 0.807 14 | 7sts 0.783 15 | 6s7s 0.688 16 | 6h9h 0.662 17 | 7sqs 0.635 18 | 7hqh 0.635 19 | 7sjs 0.605 20 | 7hjh 0.605 21 | 7h8h 0.581 22 | 6d9d 0.560 23 | 6c9c 0.560 24 | 4s6s 0.548 25 | 7hth 0.526 26 | 4h6h 0.524 27 | 3s4s 0.516 28 | 7s7h 0.493 29 | 4s7s 0.421 30 | 7sah 0.402 31 | 7sad 0.394 32 | 7sac 0.394 33 | 7has 0.390 34 | 7had 0.387 35 | 7hac 0.387 36 | 6s9s 0.318 37 | 4c6c 0.281 38 | 4d6d 0.281 39 | 6h7h 0.274 40 | 9hjh 0.245 41 | 7hkd 0.233 42 | 7hkc 0.233 43 | 7skc 0.229 44 | 7skd 0.229 45 | 6hjh 0.229 46 | 6djd 0.228 47 | 6cjc 0.228 48 | 9sjs 0.227 49 | 6sjs 0.223 50 | 9sts 0.220 51 | 9djd 0.214 52 | 9cjc 0.214 53 | 7skh 0.208 54 | 4h7h 0.206 55 | 6hqh 0.184 56 | 7sjh 0.180 57 | 7hjc 0.180 58 | 7hjs 0.180 59 | 7sjc 0.180 60 | 7hjd 0.180 61 | 7sjd 0.180 62 | 7h8c 0.177 63 | 7h8d 0.177 64 | 6hth 0.177 65 | 6sts 0.176 66 | 7hks 0.168 67 | 6sqs 0.167 68 | 3sqs 0.165 69 | 9cqc 0.163 70 | 6cqc 0.160 71 | 7s8h 0.155 72 | 7s8d 0.151 73 | 7s8c 0.151 74 | 3c6c 0.148 75 | 3d6d 0.148 76 | 7s9c 0.138 77 | 7h9c 0.138 78 | 7h9d 0.138 79 | 7s9d 0.138 80 | 7hqc 0.134 81 | 7hqs 0.134 82 | 7sqc 0.134 83 | 6dtd 0.133 84 | 6ctc 0.133 85 | 6sqh 0.133 86 | 7sqh 0.131 87 | 9hqh 0.126 88 | 4sqs 0.103 89 | 3h6h 0.101 90 | 3s6s 0.100 91 | 6sqc 0.098 92 | 7htc 0.097 93 | 7htd 0.097 94 | 3s3h 0.083 95 | tsjd 0.064 96 | tsjc 0.064 97 | 3h3c 0.064 98 | 3h3d 0.064 99 | 6hqs 0.064 100 | 9hth 0.064 101 | 9ctc 0.061 102 | 9dtd 0.061 103 | 9hqs 0.060 104 | 7s9h 0.052 105 | 7h9s 0.051 106 | tsjh 0.049 107 | 4hks 0.047 108 | 4hkh 0.047 109 | 9sqh 0.037 110 | 7stc 0.035 111 | 7std 0.035 112 | 6h9s 0.030 113 | 6h9d 0.030 114 | 6h9c 0.030 115 | 9dqs 0.030 116 | 9cqs 0.030 117 | 6c9d 0.027 118 | 6d9c 0.027 119 | 7hts 0.025 120 | tdjs 0.024 121 | tcjs 0.024 122 | 2sjs 0.023 123 | 9sjh 0.023 124 | 9hts 0.022 125 | 6dts 0.021 126 | 6cts 0.021 127 | thjs 0.020 128 | 6hts 0.019 129 | tsjs 0.018 130 | 6dqh 0.016 131 | 6cqh 0.016 132 | 6cqs 0.013 133 | 6dqs 0.013 134 | thjh 0.013 135 | tcjc 0.013 136 | tdjd 0.013 137 | 3sjs 0.012 138 | 6stc 0.011 139 | 6std 0.011 140 | 6hjs 0.010 141 | 6d9s 0.010 142 | 6c9s 0.010 143 | 4dks 0.010 144 | 4cks 0.010 145 | 6sth 0.009 146 | 6sjh 0.009 147 | 2sts 0.008 148 | 6sjc 0.008 149 | 6sjd 0.008 150 | 8dac 0.007 151 | 8cad 0.007 152 | 2sqs 0.006 153 | 2s2h 0.005 154 | 6dqc 0.005 155 | 8dah 0.005 156 | 8cah 0.005 157 | 8cas 0.003 158 | 8das 0.003 159 | 9sqc 0.002 160 | 9d9c 0.001 161 | -------------------------------------------------------------------------------- /src/nn/bucket_conversion.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import torch 4 | 5 | import settings.arguments as arguments 6 | import settings.game_settings as game_settings 7 | 8 | import game.card_tools as card_tools 9 | import nn.bucketer as bucketer 10 | 11 | 12 | class BucketConversion(object): 13 | 14 | _bucket_count: int 15 | _range_matrix: torch.Tensor 16 | _reverse_value_matrix: torch.Tensor 17 | 18 | def __init__(self): 19 | pass 20 | 21 | # --- Sets the board cards for the bucketer. 22 | # -- @param board a non-empty vector of board cards 23 | def set_board(self, board, raw=None): 24 | 25 | if raw is not None: 26 | self._bucket_count = math.comb(14, 2) + math.comb(10, 2) 27 | else: 28 | self._bucket_count = bucketer.get_bucket_count(card_tools.board_to_street(board)) 29 | 30 | self._range_matrix = arguments.Tensor(game_settings.hand_count, self._bucket_count).zero_() 31 | 32 | buckets = None 33 | if raw is not None: 34 | buckets = bucketer.compute_rank_buckets(board) 35 | else: 36 | buckets = bucketer.compute_buckets(board) 37 | 38 | class_ids = arguments.Tensor() 39 | torch.arange(1, self._bucket_count + 1, out=class_ids) 40 | 41 | class_ids = class_ids.view(1, self._bucket_count).expand(game_settings.hand_count, self._bucket_count) 42 | card_buckets = buckets.view(game_settings.hand_count, 1).expand(game_settings.hand_count, self._bucket_count) 43 | 44 | # finding all strength classes 45 | # matrix for transformation from card ranges to strength class ranges 46 | self._range_matrix[torch.eq(class_ids, card_buckets)] = 1 47 | 48 | # matrix for transformation form class values to card values 49 | self._reverse_value_matrix = self._range_matrix.t().clone() 50 | 51 | # --- Converts a range vector over private hands to a range vector over buckets. 52 | # -- 53 | # -- @{set_board} must be called first. Used to create inputs to the neural net. 54 | # -- @param card_range a probability vector over private hands 55 | # -- @param bucket_range a vector in which to save the resulting probability 56 | # -- vector over buckets 57 | def card_range_to_bucket_range(self, card_range, bucket_range): 58 | torch.mm(card_range, self._range_matrix, out=bucket_range) 59 | 60 | def hand_cfvs_to_bucket_cfvs(self, card_range, card_cfvs, bucket_range, bucketed_cfvs): 61 | torch.mm(torch.mul(card_range, card_cfvs), self._range_matrix, out=bucketed_cfvs) 62 | # avoid divide by 0 63 | bucketed_cfvs.div_(torch.clamp(bucket_range, min=0.00001)) 64 | -------------------------------------------------------------------------------- /src/nn/optimizer/adam.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | def adam(opfunc, x, config, state=None): 5 | """ An implementation of Adam http://arxiv.org/pdf/1412.6980.pdf 6 | 7 | ARGS: 8 | 9 | - 'opfunc' : a function that takes a single input (X), the point 10 | of a evaluation, and returns f(X) and df/dX 11 | - 'x' : the initial point 12 | - 'config` : a table with configuration parameters for the optimizer 13 | - 'config.learningRate' : learning rate 14 | - 'config.beta1' : first moment coefficient 15 | - 'config.beta2' : second moment coefficient 16 | - 'config.epsilon' : for numerical stability 17 | - 'config.weightDecay' : weight decay 18 | - 'state' : a table describing the state of the optimizer; after each 19 | call the state is modified 20 | 21 | RETURN: 22 | - `x` : the new x vector 23 | - `f(x)` : the value of optimized function, evaluated before the update 24 | 25 | """ 26 | # (0) get/update state 27 | if config is None and state is None: 28 | raise ValueError("adam requires a dictionary to retain state between iterations") 29 | state = state if state is not None else config 30 | lr = config.get('learningRate', 0.001) 31 | beta1 = config.get('beta1', 0.9) 32 | beta2 = config.get('beta2', 0.999) 33 | epsilon = config.get('epsilon', 1e-8) 34 | wd = config.get('weightDecay', 0) 35 | 36 | # (1) evaluate f(x) and df/dx 37 | fx, dfdx = opfunc(x) 38 | 39 | # (2) weight decay 40 | if wd != 0: 41 | dfdx.add_(wd, x) 42 | 43 | # Initialization 44 | if 't' not in state: 45 | state['t'] = 0 46 | # Exponential moving average of gradient values 47 | state['m'] = x.new().resize_as_(dfdx).zero_() 48 | # Exponential moving average of squared gradient values 49 | state['v'] = x.new().resize_as_(dfdx).zero_() 50 | # A tmp tensor to hold the sqrt(v) + epsilon 51 | state['denom'] = x.new().resize_as_(dfdx).zero_() 52 | 53 | state['t'] += 1 54 | 55 | # Decay the first and second moment running average coefficient 56 | state['m'].mul_(beta1).add_(dfdx, alpha=(1-beta1)) 57 | state['v'].mul_(beta2).addcmul_(dfdx, dfdx, value=(1-beta2)) 58 | state['denom'].copy_(state['v']).sqrt_().add_(epsilon) 59 | 60 | bias_correction1 = 1 - beta1 ** state['t'] 61 | bias_correction2 = 1 - beta2 ** state['t'] 62 | step_size = lr * math.sqrt(bias_correction2) / bias_correction1 63 | # (3) update x 64 | x.addcdiv_(state['m'], state['denom'], value=-step_size) 65 | 66 | return x, fx 67 | -------------------------------------------------------------------------------- /src/nn/modules/sequential.py: -------------------------------------------------------------------------------- 1 | 2 | import torch 3 | 4 | from nn.modules.container import Container 5 | 6 | 7 | class Sequential(Container): 8 | 9 | def __len__(self): 10 | return len(self.modules) 11 | 12 | def add(self, module): 13 | if len(self.modules) == 0: 14 | self.gradInput = module.gradInput 15 | 16 | self.modules.append(module) 17 | self.output = module.output 18 | return self 19 | 20 | def insert(self, module, index): 21 | self.modules.insert(module, index) 22 | self.output = self.modules[-1].output 23 | self.gradInput = self.modules[0].gradInput 24 | 25 | def remove(self, index=-1): 26 | del self.modules[index] 27 | 28 | if len(self.modules) > 0: 29 | self.output = self.modules[-1].output 30 | self.gradInput = self.modules[0].gradInput 31 | else: 32 | self.output = torch.Tensor() 33 | self.gradInput = torch.Tensor() 34 | 35 | def update_output(self, input): 36 | current_output = input 37 | for i, module in enumerate(self.modules): 38 | current_output = module.update_output(current_output) 39 | self.output = current_output 40 | return self.output 41 | 42 | def update_grad_input(self, input, gradOutput): 43 | current_grad_output = gradOutput 44 | for prev, current in self._iter_with_prev(): 45 | current_grad_output = current.updateGradInput(prev.output, current_grad_output) 46 | self.gradInput = self.modules[0].updateGradInput(input, current_grad_output) 47 | return self.gradInput 48 | 49 | def backward(self, input, gradOutput, scale=1): 50 | current_grad_output = gradOutput 51 | for prev, current in self._iter_with_prev(): 52 | current_grad_output = current.backward(prev.output, current_grad_output, scale) 53 | self.gradInput = self.modules[0].backward(input, current_grad_output, scale) 54 | return self.gradInput 55 | 56 | def _iter_with_prev(self): 57 | return zip(self.modules[-2::-1], self.modules[-1:0:-1]) 58 | 59 | def __repr__(self): 60 | tab = ' ' 61 | line = '\n' 62 | next = ' -> ' 63 | res = 'nn.Sequential' 64 | res = res + ' {' + line + tab + '[input' 65 | for i in range(len(self.modules)): 66 | res = res + next + '(' + str(i) + ')' 67 | 68 | res = res + next + 'output]' 69 | for i in range(len(self.modules)): 70 | res = res + line + tab + '(' + str(i) + '): ' + str(self.modules[i]).replace(line, line + tab) 71 | 72 | res = res + line + '}' 73 | return res 74 | -------------------------------------------------------------------------------- /src/player/manual_acpc_player.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import argparse 4 | sys.path.append(os.getcwd()) 5 | 6 | 7 | def run(server, port): 8 | 9 | # 1.0 connecting to the server 10 | acpc_game = ACPCGame() 11 | acpc_game.connect(server, port) 12 | 13 | state: ProcessedState 14 | winnings = 0 15 | 16 | # 2.0 main loop that waits for a situation where we act and then chooses an action 17 | while True: 18 | # 2.1 blocks until it's our situation/turn 19 | state, node, hand_winnings = acpc_game.get_next_situation() 20 | 21 | if state is None: 22 | # game ended or connection to server broke 23 | break 24 | 25 | print(Fore.WHITE + Style.BRIGHT + f"\nNew state >>> {repr(state)}") 26 | 27 | if node is not None: 28 | # 2.2 get the player's action 29 | # print("Please enter your action (f/c/#):") 30 | action = input(Fore.LIGHTBLUE_EX + Style.BRIGHT + "Please enter your next action (f/c/#): ") 31 | 32 | if action == "f": 33 | acpc_action = Action(action=constants.ACPCActions.fold) 34 | elif action == "c": 35 | acpc_action = Action(action=constants.ACPCActions.ccall, raise_amount=abs(state.bet1 - state.bet2)) 36 | else: 37 | amount = int(action) 38 | acpc_action = Action(action=constants.ACPCActions.rraise, raise_amount=amount) 39 | 40 | # 2.3 send the action to the dealer 41 | acpc_game.play_action(acpc_action) 42 | else: 43 | # hand has ended 44 | winnings += hand_winnings 45 | arguments.logger.success(f"Hand completed. Hand winnings: {hand_winnings}, Total winnings: {winnings}") 46 | print(Fore.GREEN + Style.BRIGHT + f"Hand completed. Hand winnings: {hand_winnings}, Total winnings: {winnings}") 47 | 48 | arguments.logger.success(f"Game ended >>> Total winnings: {winnings}") 49 | print(Fore.GREEN + Style.BRIGHT + f"Game ended >>> Total winnings: {winnings}") 50 | 51 | 52 | if __name__ == "__main__": 53 | parser = argparse.ArgumentParser(description='Play poker on an ACPC server') 54 | parser.add_argument('hostname', type=str, help="Hostname/IP of the server running ACPC dealer") 55 | parser.add_argument('port', type=int, help="Port to connect on the ACPC server") 56 | args = parser.parse_args() 57 | 58 | from colorama import Fore, Style 59 | 60 | import settings.arguments as arguments 61 | import settings.constants as constants 62 | 63 | from server.acpc_game import ACPCGame 64 | from server.protocol_to_node import Action, ProcessedState 65 | 66 | arguments.logger.remove(1) 67 | 68 | run(args.hostname, args.port) 69 | -------------------------------------------------------------------------------- /src/nn/net_builder.py: -------------------------------------------------------------------------------- 1 | 2 | import settings.arguments as arguments 3 | 4 | import nn.bucketer as bucketer 5 | from nn.modules.module import Module 6 | from nn.modules.batch_norm import BatchNormalization 7 | from nn.modules.linear import Linear 8 | from nn.modules.prelu import PReLU 9 | from nn.modules.sequential import Sequential 10 | from nn.modules.narrow import Narrow 11 | from nn.modules.concat_table import ConcatTable 12 | from nn.modules.add_table import CAddTable 13 | from nn.modules.dot_product import DotProduct 14 | from nn.modules.replicate import Replicate 15 | from nn.modules.mul_constant import MulConstant 16 | from nn.modules.select_table import SelectTable 17 | 18 | import utils.pseudo_random as random_ 19 | 20 | 21 | class TrainingNetwork(object): 22 | 23 | def __init__(self): 24 | pass 25 | 26 | @staticmethod 27 | def build_net(street, raw=None) -> Module: 28 | 29 | arguments.logger.trace("Building neural network") 30 | 31 | bucket_count = None 32 | if raw is not None: 33 | bucket_count = bucketer.get_rank_count() 34 | else: 35 | bucket_count = bucketer.get_bucket_count(street) 36 | 37 | player_count = 2 38 | output_size = bucket_count * player_count 39 | input_size = output_size + 1 40 | 41 | # for reproducibility 42 | if arguments.use_pseudo_random: 43 | random_.manual_seed(123) 44 | 45 | forward_part = Sequential() 46 | forward_part.add(Linear(input_size, 500)) 47 | forward_part.add(BatchNormalization(500)) 48 | forward_part.add(PReLU()) 49 | forward_part.add(Linear(500, 500)) 50 | forward_part.add(BatchNormalization(500)) 51 | forward_part.add(PReLU()) 52 | forward_part.add(Linear(500, 500)) 53 | forward_part.add(BatchNormalization(500)) 54 | forward_part.add(PReLU()) 55 | forward_part.add(Linear(500, output_size)) 56 | 57 | right_part = Sequential() 58 | right_part.add(Narrow(1, 0, output_size)) 59 | 60 | first_layer = ConcatTable() 61 | first_layer.add(forward_part) 62 | first_layer.add(right_part) 63 | 64 | left_part_2 = Sequential() 65 | left_part_2.add(SelectTable(0)) 66 | 67 | right_part_2 = Sequential() 68 | right_part_2.add(DotProduct()) 69 | right_part_2.add(Replicate(output_size, 1)) 70 | right_part_2.add(MulConstant(-0.5)) 71 | 72 | second_layer = ConcatTable() 73 | second_layer.add(left_part_2) 74 | second_layer.add(right_part_2) 75 | 76 | final_mlp = Sequential() 77 | final_mlp.add(first_layer) 78 | final_mlp.add(second_layer) 79 | final_mlp.add(CAddTable()) 80 | 81 | return final_mlp 82 | -------------------------------------------------------------------------------- /acpc_server/bm_server.config: -------------------------------------------------------------------------------- 1 | # port to connect to the server 2 | port 54000 3 | 4 | # maxmimum number of simultaneously locally running bots 5 | # 0 disables 6 | maxRunningBots 0 7 | 8 | # maximum time in seconds to wait for clients to connect when starting a match 9 | startupTimeoutSecs 100 10 | # maximum time in seconds to wait for clients to act during a match 11 | responseTimeoutSecs 600 12 | # maximum time in seconds allowed for a client to play a given hand 13 | handTimeoutSecs 21000 14 | # average time in seconds allowed for a client to spend on each hand 15 | avgHandTimeSecs 7 16 | 17 | # heads up limit Texas Hold'em 18 | game holdem.limit.2p.reverse_blinds.game { 19 | 20 | # maximum number of times a match can be run with a single player request 21 | maxMatchRuns 10 22 | 23 | # maxmimum number of simultaneously running matches using this game 24 | # 0 disables 25 | maxRunningJobs 1 26 | 27 | # number of hands in a match 28 | matchHands 5000 29 | 30 | # bot botName botStartupScript 31 | # botStartupScript is run with 3 args: server name, port, local position 32 | # local postion indicates which LOCAL bot this is (index starting from 0) 33 | # This is useful when determining which of multiple machines to run on 34 | bot testBot example_player.limit.2p.sh 35 | } 36 | 37 | # heads up limit Texas Hold'em 38 | game holdem.nolimit.2p.reverse_blinds.game { 39 | 40 | # maximum number of times a match can be run with a single player request 41 | maxMatchRuns 10 42 | 43 | # maxmimum number of simultaneously running matches using this game 44 | # 0 disables 45 | maxRunningJobs 1 46 | 47 | # number of hands in a match 48 | matchHands 5000 49 | 50 | # bot botName botStartupScript 51 | # botStartupScript is run with 3 args: server name, port, local position 52 | # local postion indicates which LOCAL bot this is (index starting from 0) 53 | # This is useful when determining which of multiple machines to run on 54 | bot testBot example_player.nolimit.2p.sh 55 | } 56 | 57 | # heads up limit Texas Hold'em 58 | game holdem.limit.3p.game { 59 | 60 | # maximum number of times a match can be run with a single player request 61 | maxMatchRuns 10 62 | 63 | # maxmimum number of simultaneously running matches using this game 64 | # 0 disables 65 | maxRunningJobs 1 66 | 67 | # number of hands in a match 68 | matchHands 5000 69 | 70 | # bot botName botStartupScript 71 | # botStartupScript is run with 3 args: server name, port, local position 72 | # local postion indicates which LOCAL bot this is (index starting from 0) 73 | # This is useful when determining which of multiple machines to run on 74 | bot testBot example_player.limit.3p.sh 75 | } 76 | 77 | # Users authorized to run jobs on the benchmark (user name pass) 78 | user neil test 79 | -------------------------------------------------------------------------------- /src/nn/bucketing/flop_tools.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import List 3 | 4 | import settings.game_settings as game_settings 5 | 6 | 7 | base_values_pow5: [] 8 | base_values_pow4: [] 9 | base_values_pow3: [] 10 | base_values_pow2: [] 11 | base_values_pow1: [] 12 | scale_factor = 13 ** 5 13 | 14 | 15 | def initialize(): 16 | global base_values_pow5 17 | global base_values_pow4 18 | global base_values_pow3 19 | global base_values_pow2 20 | global base_values_pow1 21 | 22 | base_values_pow5 = [0] * game_settings.card_count 23 | base_values_pow4 = [0] * game_settings.card_count 24 | base_values_pow3 = [0] * game_settings.card_count 25 | base_values_pow2 = [0] * game_settings.card_count 26 | base_values_pow1 = [0] * game_settings.card_count 27 | 28 | for i in range(game_settings.card_count): 29 | base_values_pow5[i] = math.floor(i / 4) * 13 * 13 * 13 * 13 30 | base_values_pow4[i] = math.floor(i / 4) * 13 * 13 * 13 31 | base_values_pow3[i] = math.floor(i / 4) * 13 * 13 32 | base_values_pow2[i] = math.floor(i / 4) * 13 33 | base_values_pow1[i] = math.floor(i / 4) 34 | 35 | 36 | initialize() 37 | 38 | 39 | def _suitcat_turn(s1, s2, s3, s4, s5): 40 | 41 | if s1 != 0: 42 | return -1 43 | 44 | ret = -1 45 | if s2 == 0: 46 | if s3 == 0: 47 | ret = s4 * 2 + s5 48 | elif s3 == 1: 49 | ret = 5 + s4 * 3 + s5 50 | elif s2 == 1: 51 | if s3 == 0: 52 | ret = 15 + s4 * 3 + s5 53 | elif s3 == 1: 54 | ret = 25 + s4 * 3 + s5 55 | elif s3 == 2: 56 | ret = 35 + s4 * 4 + s5 57 | return ret 58 | 59 | 60 | def flop_id(hand: List): 61 | # Get hand suits 62 | os = [0] * 5 63 | for i in range(5): 64 | os[i] = hand[i] % 4 65 | 66 | # Canonicalize suits 67 | MM = 0 68 | s = [0] * 5 69 | for i in range(5): 70 | j = 0 71 | while j < i: 72 | if os[i] == os[j]: 73 | s[i] = s[j] 74 | break 75 | j += 1 76 | if j == i: 77 | s[i] = MM 78 | MM += 1 79 | hand[i] += s[i] - (hand[i] % 4) 80 | hole = hand[0:2] 81 | board = hand[2:6] 82 | board.sort() 83 | hand = hole + board 84 | 85 | base_value = base(hand) 86 | 87 | for i in range(5): 88 | s[i] = hand[i] % 4 89 | 90 | cat = _suitcat_turn(s[0], s[1], s[2], s[3], s[4]) 91 | assert cat != -1, "wrong flop cat" 92 | cat = cat * scale_factor + base_value 93 | 94 | return cat 95 | 96 | 97 | def base(hand): 98 | v1 = base_values_pow5[hand[0]] 99 | v2 = base_values_pow4[hand[1]] 100 | v3 = base_values_pow3[hand[2]] 101 | v4 = base_values_pow2[hand[3]] 102 | v5 = base_values_pow1[hand[4]] 103 | return v1 + v2 + v3 + v4 + v5 104 | -------------------------------------------------------------------------------- /src/game/card_to_string_conversion.py: -------------------------------------------------------------------------------- 1 | from math import floor 2 | 3 | import torch 4 | 5 | import settings.arguments as arguments 6 | import settings.game_settings as game_settings 7 | 8 | 9 | # -- Gets the suit of a card. 10 | # -- @param card the numeric representation of the card 11 | # -- @return the index of the suit 12 | def card_to_suit(card: int): 13 | return card % game_settings.suit_count 14 | 15 | 16 | # -- Gets the rank of a card. 17 | # -- @param card the numeric representation of the card 18 | # -- @return the index of the rank 19 | def card_to_rank(card): 20 | return floor(card / game_settings.suit_count) 21 | 22 | 23 | card_to_string_table = [""] * game_settings.card_count 24 | for a_card in range(0, game_settings.card_count): 25 | rank_name = game_settings.rank_table[card_to_rank(a_card)] 26 | suit_name = game_settings.suit_table[card_to_suit(a_card)] 27 | card_to_string_table[a_card] = rank_name + suit_name 28 | 29 | string_to_card_table = {} 30 | for a_card in range(0, game_settings.card_count): 31 | string_to_card_table[card_to_string_table[a_card]] = a_card 32 | 33 | 34 | # -- Converts a card's numeric representation to its string representation. 35 | # -- @param card the numeric representation of a card 36 | # -- @return the string representation of the card 37 | def card_to_string(card): 38 | assert (0 <= card < game_settings.card_count) 39 | return card_to_string_table[card] 40 | 41 | 42 | # -- Converts several cards' numeric representations to their string 43 | # -- representations. 44 | # -- @param cards a vector of numeric representations of cards 45 | # -- @return a string containing each card's string representation, concatenated 46 | def cards_to_string(cards: torch.Tensor) -> str: 47 | if cards.dim() == 0: 48 | return "" 49 | 50 | out = "" 51 | for card in range(0, cards.size(0)): 52 | out = out + card_to_string(cards[card].int()) 53 | return out 54 | 55 | 56 | # --- Converts a string representing zero or more board cards to a 57 | # -- vector of numeric representations. 58 | # -- @param card_string either the empty string or a string representation of a 59 | # -- card 60 | # -- @return either an empty tensor or a tensor containing the numeric 61 | # -- representation of the card 62 | def string_to_board(card_string): 63 | # assert card_string 64 | if card_string == '': 65 | return arguments.Tensor() 66 | 67 | num_cards = int(len(card_string) / 2) 68 | board = arguments.Tensor(num_cards) 69 | for i in range(0, num_cards): 70 | board[i] = string_to_card(card_string[i * 2: (i + 1) * 2]) 71 | return board.long() 72 | 73 | 74 | # --- Converts a card's string representation to its numeric representation. 75 | # -- @param card_string the string representation of a card 76 | # -- @return the numeric representation of the card 77 | def string_to_card(card_string: str): 78 | card_index = string_to_card_table[card_string] 79 | assert 0 <= card_index < game_settings.card_count 80 | return card_index 81 | 82 | -------------------------------------------------------------------------------- /src/player/dyypholdem_slumbot_player.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import argparse 4 | sys.path.append(os.getcwd()) 5 | 6 | 7 | def play_hand(token, hand): 8 | 9 | winnings = 0 10 | 11 | response = slumbot_game.new_hand(token) 12 | new_token = response.get('token') 13 | if new_token: 14 | token = new_token 15 | arguments.logger.trace(f"Current token: {token}") 16 | 17 | current_state, current_node = slumbot_game.get_next_situation(response) 18 | 19 | winnings = response.get('winnings') 20 | # game goes on 21 | if winnings is None: 22 | 23 | arguments.logger.info(f"Starting new hand #{hand+1}") 24 | continual_resolving.start_new_hand(current_state) 25 | 26 | while True: 27 | # use continual resolving to find a strategy and make an action in the current node 28 | advised_action: protocol_to_node.Action = continual_resolving.compute_action(current_state, current_node) 29 | 30 | # send the action to the server 31 | response = slumbot_game.play_action(token, advised_action) 32 | current_state, current_node = slumbot_game.get_next_situation(response) 33 | 34 | winnings = response.get('winnings') 35 | if winnings is not None: 36 | # hand has ended 37 | break 38 | 39 | # clean up and release memory 40 | if arguments.use_gpu: 41 | arguments.logger.trace(f"Initiating garbage collection. Allocated memory={torch.cuda.memory_allocated('cuda')}, Reserved memory={torch.cuda.memory_reserved('cuda')}") 42 | del current_node 43 | del current_state 44 | gc.collect() 45 | if arguments.use_gpu: 46 | torch.cuda.empty_cache() 47 | arguments.logger.trace(f"Garbage collection performed. Allocated memory={torch.cuda.memory_allocated('cuda')}, Reserved memory={torch.cuda.memory_reserved('cuda')}") 48 | 49 | return token, winnings 50 | 51 | 52 | def play_slumbot(): 53 | token = None 54 | num_hands = args.hands 55 | winnings = 0 56 | for hand in range(num_hands): 57 | token, hand_winnings = play_hand(token, hand) 58 | winnings += hand_winnings 59 | arguments.logger.success(f"Hand completed. Hand winnings: {hand_winnings}, Total winnings: {winnings} ") 60 | 61 | arguments.logger.success(f"Game ended >>> Total winnings: {winnings}") 62 | 63 | 64 | if __name__ == '__main__': 65 | parser = argparse.ArgumentParser(description='Play with DyypHoldem against Slumbot') 66 | parser.add_argument('hands', type=int, help="Number of hands to play against Slumbot") 67 | args = parser.parse_args() 68 | 69 | import gc 70 | 71 | import torch 72 | 73 | import settings.arguments as arguments 74 | 75 | from server.slumbot_game import SlumbotGame 76 | import server.protocol_to_node as protocol_to_node 77 | from lookahead.continual_resolving import ContinualResolving 78 | 79 | slumbot_game = SlumbotGame() 80 | continual_resolving = ContinualResolving() 81 | 82 | play_slumbot() 83 | -------------------------------------------------------------------------------- /acpc_server/play_match.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # Copyright (C) 2011 by the Computer Poker Research Group, University of Alberta 4 | 5 | use Socket; 6 | 7 | $hostname = `hostname` or die "could not get hostname"; 8 | chomp $hostname; 9 | @hostent = gethostbyname( $hostname ); 10 | $#hostent >= 4 or die "could not look up $hostname"; 11 | $hostip = inet_ntoa( $hostent[ 4 ] ); 12 | 13 | $#ARGV >= 3 or die "usage: play_match.pl matchName gameDefFile #Hands rngSeed player1name player1exe player2name player2exe ... [options]"; 14 | 15 | $numPlayers = -1; 16 | open FILE, '<', $ARGV[ 1 ] or die "couldn't open game definition $ARGV[ 1 ]"; 17 | while( $_ = ) { 18 | 19 | @_ = split; 20 | 21 | if( uc( $_[ 0 ] ) eq 'NUMPLAYERS' ) { 22 | $numPlayers = $_[ $#_ ]; 23 | } 24 | } 25 | close FILE; 26 | 27 | $numPlayers > 1 or die "couldn't get number of players from $ARGV[ 1 ]"; 28 | 29 | 30 | $#ARGV >= 3 + $numPlayers * 2 or die "too few players on command line"; 31 | 32 | pipe STDINREADPIPE, STDINWRITEPIPE or die "couldn't create stdin pipe"; 33 | pipe STDOUTREADPIPE, STDOUTWRITEPIPE or die "couldn't create stdout pipe"; 34 | 35 | $dealerPID = fork(); 36 | if( $dealerPID == 0 ) { 37 | # we're the child 38 | 39 | # replace standard in and standard out with pipe 40 | close STDINWRITEPIPE; 41 | close STDOUTREADPIPE; 42 | open STDIN, '<&STDINREADPIPE' or die "can't dup STDIN"; 43 | open STDOUT, '>&STDOUTWRITEPIPE' or die "can't dup STDOUT"; 44 | open STDERR, ">>$ARGV[ 0 ].err" or die "can't open log file $ARGV[ 0 ].err"; 45 | 46 | @args = ( "dealer", $ARGV[ 0 ], $ARGV[ 1 ], 47 | $ARGV[ 2 ], $ARGV[ 3 ] ); 48 | 49 | # add names to the arguments 50 | for( $p = 0; $p < $numPlayers; ++$p ) { 51 | push @args, $ARGV[ 4 + $p * 2 ]; 52 | } 53 | 54 | # add any extra arguments (options?) to the arguments 55 | for( $i = 4 + $numPlayers * 2; $i <= $#ARGV; ++$i ) { 56 | push @args, $ARGV[ $i ]; 57 | } 58 | exec { "./dealer" } @args or die "Couldn't run dealer"; 59 | } 60 | 61 | close STDINREADPIPE; 62 | close STDOUTWRITEPIPE; 63 | 64 | $_ = or die "couldn't read port description from dealer"; 65 | @_ = split; 66 | $#_ + 1 >= $numPlayers or die "couldn't get enough ports from $_"; 67 | 68 | for( $p = 0; $p < $numPlayers; ++$p ) { 69 | 70 | $playerPID[ $p ] = fork(); 71 | 72 | if( $playerPID[ $p ] == 0 ) { 73 | # we're the child 74 | 75 | # log standard out and standard error 76 | open STDOUT, ">$ARGV[ 0 ].player$p.std" 77 | or die "can't dup player $p STDOUT"; 78 | open STDERR, ">$ARGV[ 0 ].player$p.err" 79 | or die "can't dup player $p STDERR"; 80 | 81 | exec { $ARGV[ 4 + $p * 2 + 1 ] } ( $ARGV[ 4 + $p * 2 + 1 ], 82 | $hostip, $_[ $p ] ) 83 | or die "couldn't run $ARGV[ 4 + $p * 2 + 1 ] for player $p"; 84 | } 85 | } 86 | 87 | $_ = ; 88 | 89 | for( $p = 0; $p < $numPlayers; ++$p ) { 90 | waitpid( $playerPID[ $p ], 0 ); 91 | } 92 | 93 | waitpid( $dealerPID, 0 ); 94 | 95 | $_ or die "couldn't get values from dealer"; 96 | 97 | print $_; 98 | 99 | exit( 0 ); 100 | -------------------------------------------------------------------------------- /src/utils/pseudo_random.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random as rng 3 | 4 | import torch 5 | 6 | import settings.arguments as arguments 7 | 8 | 9 | # coefficients for MT19937 10 | (w, n, m, r) = (32, 624, 397, 31) 11 | a = 0x9908B0DF 12 | (u, d) = (11, 0xFFFFFFFF) 13 | (s, b) = (7, 0x9D2C5680) 14 | (t, c) = (15, 0xEFC60000) 15 | l = 18 16 | f = 1812433253 17 | 18 | # make an array to store the state of the generator 19 | MT = [0 for i in range(n)] 20 | index = n+1 21 | lower_mask = 0x7FFFFFFF # (1 << r) - 1 // That is, the binary number of r 1's 22 | upper_mask = 0x80000000 # lowest w bits of (not lower_mask) 23 | 24 | 25 | # initialize the generator from a seed 26 | def manual_seed(seed): 27 | # global index 28 | # index = n 29 | MT[0] = seed 30 | for i in range(1, n): 31 | temp = f * (MT[i-1] ^ (MT[i-1] >> (w-2))) + i 32 | MT[i] = temp & 0xffffffff 33 | 34 | 35 | # Extract a tempered value based on MT[index] 36 | # calling twist() every n numbers 37 | def extract_number(): 38 | global index 39 | if index >= n: 40 | twist() 41 | index = 0 42 | 43 | y = MT[index] 44 | y = y ^ ((y >> u) & d) 45 | y = y ^ ((y << s) & b) 46 | y = y ^ ((y << t) & c) 47 | y = y ^ (y >> l) 48 | 49 | index += 1 50 | return y & 0xffffffff 51 | 52 | 53 | # Generate the next n values from the series x_i 54 | def twist(): 55 | for i in range(0, n): 56 | x = (MT[i] & upper_mask) + (MT[(i+1) % n] & lower_mask) 57 | xA = x >> 1 58 | if (x % 2) != 0: 59 | xA = xA ^ a 60 | MT[i] = MT[(i + m) % n] ^ xA 61 | 62 | 63 | # return a single random number in the range 0, 1 64 | def random(): 65 | if arguments.use_pseudo_random: 66 | return extract_number() / (2 ** 32) 67 | else: 68 | return torch.rand(1).item() 69 | 70 | 71 | # return tensor filled with size random elements 72 | def rand(size): 73 | if arguments.use_pseudo_random: 74 | ret = arguments.Tensor(size) 75 | for i in range(size): 76 | ret[i] = random() 77 | else: 78 | ret = torch.rand(size, device=arguments.device) 79 | return ret 80 | 81 | 82 | # return a single random integer in the range 0, 1 83 | def randint(low, high): 84 | if arguments.use_pseudo_random: 85 | val = random() 86 | val *= (high - low + 1) 87 | val = math.floor(val) + low 88 | else: 89 | val = rng.randint(low, high) 90 | return int(val) 91 | 92 | 93 | def uniform(size, low=0.0, high=1.0): 94 | ret = arguments.Tensor(size).fill_(0.0) 95 | for i in range(size): 96 | rnd = random() 97 | rnd = (rnd * (high - low)) + low 98 | ret[i] = rnd 99 | return ret 100 | 101 | 102 | def uniform_(src: torch.Tensor, low=0.0, high=1.0): 103 | src.fill_(0.0) 104 | vw = src.view(-1) 105 | for i in range(len(vw)): 106 | rnd = random() 107 | rnd = (rnd * (high - low)) + low 108 | vw[i] = rnd 109 | 110 | 111 | if __name__ == '__main__': 112 | manual_seed(123) 113 | arguments.logger.info(f"Extracted number: {extract_number()}") 114 | -------------------------------------------------------------------------------- /acpc_server/acpc_play_match.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # Copyright (C) 2011 by the Computer Poker Research Group, University of Alberta 4 | 5 | use Socket; 6 | use File::Basename; 7 | 8 | $hostname = `hostname` or die "could not get hostname"; 9 | chomp $hostname; 10 | @hostent = gethostbyname( $hostname ); 11 | $#hostent >= 4 or die "could not look up $hostname"; 12 | $hostip = inet_ntoa( $hostent[ 4 ] ); 13 | 14 | $#ARGV >= 3 or die "usage: play_match.pl matchName gameDefFile #Hands rngSeed player1name player1exe player2name player2exe ... [options]"; 15 | 16 | $numPlayers = -1; 17 | open FILE, '<', $ARGV[ 1 ] or die "couldn't open game definition $ARGV[ 1 ]"; 18 | while( $_ = ) { 19 | 20 | @_ = split; 21 | 22 | if( uc( $_[ 0 ] ) eq 'NUMPLAYERS' ) { 23 | $numPlayers = $_[ $#_ ]; 24 | } 25 | } 26 | close FILE; 27 | 28 | $numPlayers > 1 or die "couldn't get number of players from $ARGV[ 1 ]"; 29 | 30 | 31 | $#ARGV >= 3 + $numPlayers * 2 or die "too few players on command line"; 32 | 33 | pipe STDINREADPIPE, STDINWRITEPIPE or die "couldn't create stdin pipe"; 34 | pipe STDOUTREADPIPE, STDOUTWRITEPIPE or die "couldn't create stdout pipe"; 35 | 36 | $dealerPID = fork(); 37 | if( $dealerPID == 0 ) { 38 | # we're the child 39 | 40 | # replace standard in and standard out with pipe 41 | close STDINWRITEPIPE; 42 | close STDOUTREADPIPE; 43 | open STDIN, '<&STDINREADPIPE' or die "can't dup STDIN"; 44 | open STDOUT, '>&STDOUTWRITEPIPE' or die "can't dup STDOUT"; 45 | open STDERR, ">>$ARGV[ 0 ].err" or die "can't open log file $ARGV[ 0 ].err"; 46 | 47 | @args = ( "dealer", $ARGV[ 0 ], $ARGV[ 1 ], 48 | $ARGV[ 2 ], $ARGV[ 3 ] ); 49 | 50 | # add names to the arguments 51 | for( $p = 0; $p < $numPlayers; ++$p ) { 52 | push @args, $ARGV[ 4 + $p * 2 ]; 53 | } 54 | 55 | # add any extra arguments (options?) to the arguments 56 | for( $i = 4 + $numPlayers * 2; $i <= $#ARGV; ++$i ) { 57 | push @args, $ARGV[ $i ]; 58 | } 59 | exec { "./dealer" } @args or die "Couldn't run dealer"; 60 | } 61 | 62 | close STDINREADPIPE; 63 | close STDOUTWRITEPIPE; 64 | 65 | $_ = or die "couldn't read port description from dealer"; 66 | @_ = split; 67 | $#_ + 1 >= $numPlayers or die "couldn't get enough ports from $_"; 68 | 69 | for( $p = 0; $p < $numPlayers; ++$p ) { 70 | 71 | $playerPID[ $p ] = fork(); 72 | 73 | if( $playerPID[ $p ] == 0 ) { 74 | # we're the child 75 | 76 | # log standard out and standard error 77 | open STDOUT, ">$ARGV[ 0 ].player$p.std" 78 | or die "can't dup player $p STDOUT"; 79 | open STDERR, ">$ARGV[ 0 ].player$p.err" 80 | or die "can't dup player $p STDERR"; 81 | 82 | ($playerExec, $playerDir) = fileparse( $ARGV[ 4 + $p * 2 + 1 ] ); 83 | chdir $playerDir or die "Can't cd to $playerDir: $!\n"; 84 | exec { "./$playerExec" } ( "./$playerExec", $hostip, $_[ $p ] ) 85 | or die "couldn't run $playerExec from $playerDir for player $p"; 86 | } 87 | } 88 | 89 | $_ = ; 90 | 91 | for( $p = 0; $p < $numPlayers; ++$p ) { 92 | waitpid( $playerPID[ $p ], 0 ); 93 | } 94 | 95 | waitpid( $dealerPID, 0 ); 96 | 97 | $_ or die "couldn't get values from dealer"; 98 | 99 | print $_; 100 | 101 | exit( 0 ); 102 | -------------------------------------------------------------------------------- /src/game/bet_sizing.py: -------------------------------------------------------------------------------- 1 | 2 | import torch 3 | 4 | import settings.arguments as arguments 5 | import settings.game_settings as game_settings 6 | 7 | 8 | class BetSizing(object): 9 | 10 | pot_fractions: list 11 | 12 | # --- Constructor 13 | # -- @param pot_fractions a list of fractions of the pot which are allowed 14 | # -- as bets, sorted in ascending order 15 | def __init__(self, pot_fractions): 16 | self.pot_fractions = pot_fractions or [1] 17 | 18 | # --- Gives the bets which are legal at a game state. 19 | # -- @param node a representation of the current game state, with fields: 20 | # -- 21 | # -- * `bets`: the number of chips currently committed by each player 22 | # -- 23 | # -- * `current_player`: the currently acting player 24 | # -- @return an Nx2 tensor where N is the number of new possible game states, 25 | # -- containing N sets of new commitment levels for each player 26 | def get_possible_bets(self, node): 27 | current_player = node.current_player.value 28 | assert current_player == 0 or current_player == 1, 'Wrong player for bet size computation' 29 | opponent = 1 - node.current_player.value 30 | opponent_bet = node.bets[opponent] 31 | assert node.bets[current_player] <= opponent_bet, "Not a betting situation" 32 | 33 | # compute min possible raise size 34 | max_raise_size = game_settings.stack - opponent_bet 35 | min_raise_size = opponent_bet - node.bets[current_player] 36 | min_raise_size = max(min_raise_size, game_settings.ante) 37 | min_raise_size = min(max_raise_size, min_raise_size) 38 | 39 | if min_raise_size == 0: 40 | return torch.tensor(0) # hack to create 0-dimensional tensor 41 | elif min_raise_size == max_raise_size: 42 | out = arguments.Tensor(1, 2).fill_(opponent_bet) 43 | out[0][current_player] = opponent_bet + min_raise_size 44 | return out 45 | else: 46 | # iterate through all bets and check if they are possible 47 | fractions = [] 48 | if node.num_bets == 0: 49 | fractions = self.pot_fractions[0] 50 | elif node.num_bets == 1: 51 | fractions = self.pot_fractions[1] 52 | else: 53 | fractions = self.pot_fractions[2] 54 | 55 | max_possible_bets_count = len(fractions) + 1 # we can always go allin 56 | out = arguments.Tensor(max_possible_bets_count, 2).fill_(opponent_bet) 57 | 58 | # take pot size after opponent bet is called 59 | pot = opponent_bet * 2 60 | used_bets_count = -1 61 | 62 | # try all pot fractions bet and see if we can use them 63 | for i in range(0, len(fractions)): 64 | raise_size = pot * fractions[i] 65 | if min_raise_size <= raise_size < max_raise_size: 66 | used_bets_count = used_bets_count + 1 67 | out[used_bets_count, current_player] = opponent_bet + raise_size 68 | 69 | # adding allin 70 | used_bets_count = used_bets_count + 1 71 | assert used_bets_count <= max_possible_bets_count, "Wrong number of bets" 72 | out[used_bets_count, current_player] = opponent_bet + max_raise_size 73 | 74 | return out[0:used_bets_count + 1, :] 75 | -------------------------------------------------------------------------------- /src/nn/modules/linear.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import settings.arguments as arguments 4 | 5 | from nn.modules.module import Module 6 | from nn.modules.utils import clear 7 | 8 | import utils.pseudo_random as pseudo_random 9 | 10 | 11 | class Linear(Module): 12 | 13 | def __init__(self, inputSize, outputSize, bias=True): 14 | super(Linear, self).__init__() 15 | self.weight = arguments.Tensor(outputSize, inputSize) 16 | self.gradWeight = arguments.Tensor(outputSize, inputSize) 17 | self.bias = arguments.Tensor(outputSize) if bias else None 18 | self.gradBias = arguments.Tensor(outputSize) if bias else None 19 | self.reset() 20 | 21 | self.addBuffer = None 22 | 23 | def update_output(self, input): 24 | assert input.dim() == 2 25 | 26 | self.output = arguments.Tensor(input.size(0), self.weight.size(0)).zero_() 27 | self._update_addBuffer(input) 28 | self.output.addmm_(input, self.weight.t(), alpha=1.0, beta=0.0) 29 | 30 | if self.bias is not None: 31 | self.output.addr_(self.addBuffer, self.bias) 32 | 33 | return self.output 34 | 35 | def update_grad_input(self, input, gradOutput): 36 | if self.gradInput is None: 37 | return 38 | 39 | nelement = self.gradInput.nelement() 40 | self.gradInput.resize_as_(input) 41 | if self.gradInput.nelement() != nelement: 42 | self.gradInput.zero_() 43 | 44 | assert input.dim() == 2 45 | self.gradInput.addmm_(gradOutput, self.weight, alpha=1.0, beta=0.0) 46 | 47 | return self.gradInput 48 | 49 | def acc_grad_parameters(self, input, gradOutput, scale=1): 50 | # serialization.serialize_as_tmp_t7("gradOutput-modern", gradOutput) 51 | assert input.dim() == 2 52 | # self.gradWeight.addmm_(scale, gradOutput.t(), input) # deprecated 53 | self.gradWeight.addmm_(gradOutput.t(), input, alpha=scale) 54 | if self.bias is not None: 55 | # update the size of addBuffer if the input is not the same size as the one we had in last updateGradInput 56 | self._update_addBuffer(input) 57 | # self.gradBias.addmv_(scale, gradOutput.t(), self.addBuffer) # depreacted 58 | self.gradBias.addmv_(gradOutput.t(), self.addBuffer, alpha=scale) 59 | 60 | def no_bias(self): 61 | self.bias = None 62 | self.gradBias = None 63 | return self 64 | 65 | def reset(self, stdv=None): 66 | arguments.logger.trace(f"Resetting 'Linear' module with size {repr(self.weight.size())}") 67 | if stdv is not None: 68 | stdv = stdv * math.sqrt(3) 69 | else: 70 | stdv = 1. / math.sqrt(self.weight.size(1)) 71 | 72 | if arguments.use_pseudo_random: 73 | pseudo_random.uniform_(self.weight, -stdv, stdv) 74 | if self.bias is not None: 75 | pseudo_random.uniform_(self.bias, -stdv, stdv) 76 | else: 77 | self.weight.uniform_(-stdv, stdv) 78 | if self.bias is not None: 79 | self.bias.uniform_(-stdv, stdv) 80 | return self 81 | 82 | def clear_state(self): 83 | clear(self, 'addBuffer') 84 | return super(Linear, self).clear_state() 85 | 86 | def _update_addBuffer(self, input): 87 | self.addBuffer = input.new(input.size(0)).fill_(1) 88 | 89 | def __repr__(self): 90 | return super(Linear, self).__repr__() + \ 91 | '({} -> {})'.format(self.weight.size(1), self.weight.size(0)) + \ 92 | (' without bias' if self.bias is None else '') 93 | -------------------------------------------------------------------------------- /src/nn/modules/masked_huber_loss.py: -------------------------------------------------------------------------------- 1 | 2 | import torch 3 | 4 | import settings.arguments as arguments 5 | 6 | from nn.modules.criterion import Criterion 7 | from nn.modules.smooth_loss import SmoothL1Criterion 8 | 9 | 10 | class MaskedHuberLoss(Criterion): 11 | 12 | mask_sum: torch.Tensor 13 | 14 | def __init__(self): 15 | super().__init__() 16 | self.criterion = SmoothL1Criterion() 17 | self.mask_sum = None 18 | self.mask_placeholder = None 19 | self.mask_multiplier = None 20 | 21 | # --- Computes the loss over a batch of neural net outputs and targets. 22 | # -- 23 | # -- @param outputs an NxM tensor containing N vectors of values over buckets, 24 | # -- output by the neural net 25 | # -- @param targets an NxM tensor containing N vectors of actual values over 26 | # -- buckets, produced by @{data_generation_call} 27 | # -- @param mask an NxM tensor containing N mask vectors generated with 28 | # -- @{bucket_conversion.get_possible_bucket_mask} 29 | # -- @return the sum of Huber loss applied elementwise on `outputs` and `targets`, 30 | # -- masked so that only valid buckets are included 31 | def forward(self, outputs, targets, mask=None): 32 | 33 | batch_size = outputs.size(0) 34 | feature_size = outputs.size(1) 35 | 36 | # 1.0 zero out the outputs/target so that the error does not depend on these 37 | outputs.mul_(mask) 38 | targets.mul(mask) 39 | 40 | loss = self.criterion.forward(outputs, targets) 41 | 42 | # 2.0 if the batch size has changed, create new storage for the sum, otherwise reuse 43 | if self.mask_sum is None or (self.mask_sum.size(0) != batch_size): 44 | self.mask_placeholder = arguments.Tensor(mask.size()).fill_(0) 45 | self.mask_sum = arguments.Tensor(batch_size).fill_(0) 46 | self.mask_multiplier = self.mask_sum.clone().fill_(0).view(-1, 1) 47 | 48 | # 3.0 compute mask sum for each batch 49 | self.mask_placeholder.copy_(mask) 50 | self.mask_sum = torch.sum(self.mask_placeholder, 1) 51 | 52 | # 3.1 mask multiplier - note that mask is 1 for impossible features 53 | self.mask_multiplier.fill_(feature_size) 54 | self.mask_multiplier.div_(self.mask_sum.view(self.mask_multiplier.shape)) 55 | 56 | # 4.0 multiply to get a new loss 57 | # loss is not really computed batch-wise correctly, 58 | # but that does not really matter now since gradients are correct 59 | loss_multiplier = (batch_size * feature_size) / self.mask_sum.sum() 60 | new_loss = loss_multiplier * loss 61 | 62 | return new_loss 63 | 64 | # --- Computes the gradient of the loss function @{forward} with 65 | # -- arguments `outputs`, `targets`, and `mask`. 66 | # -- 67 | # -- Must be called after a @{forward} call with the same arguments. 68 | # -- 69 | # -- @param outputs an NxM tensor containing N vectors of values over buckets, 70 | # -- output by the neural net 71 | # -- @param targets an NxM tensor containing N vectors of actual values over 72 | # -- buckets, produced by @{data_generation_call} 73 | # -- @param mask an NxM tensor containing N mask vectors generated with 74 | # -- @{bucket_conversion.get_possible_bucket_mask} 75 | # -- @return the gradient of @{forward} applied to the arguments 76 | def backward(self, outputs, targets): 77 | dloss_doutput = self.criterion.backward(outputs, targets) 78 | # we use the multiplier computed with the mask during forward call 79 | dloss_doutput.mul_(self.mask_multiplier.expand_as(dloss_doutput)) 80 | 81 | return dloss_doutput 82 | -------------------------------------------------------------------------------- /src/nn/bucketing/turn_tools.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import List 3 | 4 | import settings.game_settings as game_settings 5 | 6 | base_values_pow6: [] 7 | base_values_pow5: [] 8 | base_values_pow4: [] 9 | base_values_pow3: [] 10 | base_values_pow2: [] 11 | base_values_pow1: [] 12 | scale_factor = 13 ** 6 13 | 14 | 15 | def initialize(): 16 | global base_values_pow6 17 | global base_values_pow5 18 | global base_values_pow4 19 | global base_values_pow3 20 | global base_values_pow2 21 | global base_values_pow1 22 | 23 | base_values_pow6 = [0] * game_settings.card_count 24 | base_values_pow5 = [0] * game_settings.card_count 25 | base_values_pow4 = [0] * game_settings.card_count 26 | base_values_pow3 = [0] * game_settings.card_count 27 | base_values_pow2 = [0] * game_settings.card_count 28 | base_values_pow1 = [0] * game_settings.card_count 29 | 30 | for i in range(game_settings.card_count): 31 | base_values_pow6[i] = math.floor(i / 4) * 13 * 13 * 13 * 13 * 13 32 | base_values_pow5[i] = math.floor(i / 4) * 13 * 13 * 13 * 13 33 | base_values_pow4[i] = math.floor(i / 4) * 13 * 13 * 13 34 | base_values_pow3[i] = math.floor(i / 4) * 13 * 13 35 | base_values_pow2[i] = math.floor(i / 4) * 13 36 | base_values_pow1[i] = math.floor(i / 4) 37 | 38 | 39 | initialize() 40 | 41 | 42 | def _suitcat_turn(s1, s2, s3, s4, s5, s6): 43 | 44 | if s1 != 0: 45 | return -1 46 | 47 | ret = -1 48 | if s2 == 0: 49 | if s3 == 0: 50 | if s4 == 0: 51 | ret = s5 * 2 + s6 52 | elif s4 == 1: 53 | ret = 5 + s5 * 3 + s6 54 | elif s3 == 1: 55 | if s4 == 0: 56 | ret = 15 + s5 * 3 + s6 57 | elif s4 == 1: 58 | ret = 25 + s5 * 3 + s6 59 | elif s4 == 2: 60 | ret = 35 + s5 * 4 + s6 61 | elif s2 == 1: 62 | if s3 == 0: 63 | if s4 == 0: 64 | ret = 51 + s5 * 3 + s6 65 | elif s4 == 1: 66 | ret = 61 + s5 * 3 + s6 67 | elif s4 == 2: 68 | ret = 71 + s5 * 4 + s6 69 | elif s3 == 1: 70 | if s4 == 0: 71 | ret = 87 + s5 * 3 + s6 72 | elif s4 == 1: 73 | ret = 97 + s5 * 3 + s6 74 | elif s4 == 2: 75 | ret = 107 + s5 * 4 + s6 76 | elif s3 == 2: 77 | ret = 123 + s4 * 16 + s5 * 4 + s6 78 | return ret 79 | 80 | 81 | def turn_id(hand: List): 82 | # Get hand suits 83 | os = [0] * 6 84 | for i in range(6): 85 | os[i] = hand[i] % 4 86 | 87 | # Canonicalize suits 88 | MM = 0 89 | s = [0] * 6 90 | for i in range(6): 91 | j = 0 92 | while j < i: 93 | if os[i] == os[j]: 94 | s[i] = s[j] 95 | break 96 | j += 1 97 | if j == i: 98 | s[i] = MM 99 | MM += 1 100 | hand[i] += s[i] - (hand[i] % 4) 101 | hole = hand[0:2] 102 | board = hand[2:6] 103 | board.sort() 104 | hand = hole + board 105 | 106 | base_value = base(hand) 107 | 108 | for i in range(6): 109 | s[i] = hand[i] % 4 110 | 111 | cat = _suitcat_turn(s[0], s[1], s[2], s[3], s[4], s[5]) 112 | assert cat != -1, "wrong turn cat" 113 | cat = cat * scale_factor + base_value 114 | 115 | return cat 116 | 117 | 118 | def base(hand): 119 | v1 = base_values_pow6[hand[0]] 120 | v2 = base_values_pow5[hand[1]] 121 | v3 = base_values_pow4[hand[2]] 122 | v4 = base_values_pow3[hand[3]] 123 | v5 = base_values_pow2[hand[4]] 124 | v6 = base_values_pow1[hand[5]] 125 | return v1 + v2 + v3 + v4 + v5 + v6 126 | -------------------------------------------------------------------------------- /src/utils/output.py: -------------------------------------------------------------------------------- 1 | 2 | import settings.arguments as arguments 3 | import settings.game_settings as game_settings 4 | 5 | import game.card_tools as card_tools 6 | import game.card_to_string_conversion as card_to_string 7 | 8 | 9 | class DummyLogger(object): 10 | 11 | def __init__(self, log_level): 12 | self.log_level = self._level_as_number(log_level) 13 | 14 | @staticmethod 15 | def _level_as_number(level) -> int: 16 | if level == 'TRACE': 17 | return 5 18 | elif level == 'LOADING': 19 | return 8 20 | elif level == 'DEBUG': 21 | return 10 22 | elif level == 'TIMING': 23 | return 15 24 | elif level == 'INFO': 25 | return 20 26 | elif level == 'SUCCESS': 27 | return 25 28 | elif level == 'WARNING': 29 | return 30 30 | elif level == 'ERROR': 31 | return 40 32 | elif level == 'CRITICAL': 33 | return 50 34 | 35 | def log(self, level, msg): 36 | _level = self._level_as_number(level) 37 | if _level >= self.log_level: 38 | if level == 'TRACE': 39 | print(f"TRACE: {msg}") 40 | elif level == 'LOADING': 41 | print(f"LOADING: {msg}") 42 | elif level == 'DEBUG': 43 | print(f"DEBUG: {msg}") 44 | elif level == 'TIMING': 45 | print(f"TIMING: {msg}") 46 | elif level == 'INFO': 47 | print(f"INFO: {msg}") 48 | elif level == 'SUCCESS': 49 | print(f"SUCCESS: {msg}") 50 | elif level == 'WARNING': 51 | print(f"WARNING: {msg}") 52 | elif level == 'ERROR': 53 | print(f"ERROR: {msg}") 54 | elif level == 'CRITICAL': 55 | print(f"CRITICAL: {msg}") 56 | 57 | def trace(self, msg): 58 | self.log('TRACE', msg) 59 | 60 | def loading(self, msg): 61 | self.log('LOADING', msg) 62 | 63 | def debug(self, msg): 64 | self.log('DEBUG', msg) 65 | 66 | def timing(self, msg): 67 | self.log('TIMING', msg) 68 | 69 | def info(self, msg): 70 | self.log('INFO', msg) 71 | 72 | def success(self, msg): 73 | self.log('SUCCESS', msg) 74 | 75 | def warning(self, msg): 76 | self.log('WARNING', msg) 77 | 78 | def error(self, msg): 79 | self.log('ERROR', msg) 80 | 81 | def critical(self, msg): 82 | self.log('CRITICAL', msg) 83 | 84 | 85 | def show_results(player_range, node, results): 86 | for card1 in range(game_settings.card_count): 87 | for card2 in range(card1 + 1, game_settings.card_count): 88 | idx = card_tools.get_hole_index([card1, card2]) 89 | if player_range[0][idx] > 0: 90 | result = f"{card_to_string.card_to_string(card1)}{card_to_string.card_to_string(card2)}: {'{:.3f}'.format(results.root_cfvs_both_players[0][0][idx])}" 91 | for i in range(results.strategy.size(0)): 92 | action = int(results.get_actions()[i]) 93 | if action == -2: 94 | action_str = " \tFold" 95 | elif action == -1: 96 | if node.bets[0] == node.bets[1]: 97 | action_str = "Check" 98 | else: 99 | action_str = "Call" 100 | elif action == game_settings.stack: 101 | action_str = "All-In" 102 | else: 103 | action_str = f"Bet {action}" 104 | result += f" {action_str}: {results.strategy[i][0][idx]:.6f}" 105 | arguments.logger.info(result) 106 | -------------------------------------------------------------------------------- /src/tests/dyypholdem_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | sys.path.append(os.getcwd()) 4 | 5 | 6 | last_state = None 7 | last_node = None 8 | 9 | # game_messages = ["MATCHSTATE:0:0:r300:Qs9d|", "MATCHSTATE:0:0:r300c/:Qs9d|/6d4d3c"] 10 | game_messages = ["MATCHSTATE:1:3::|TcAd", "MATCHSTATE:1:3:r300c/c:|TcAd/Ts8c6h", "MATCHSTATE:1:3:r300c/cc/r900:|TcAd/Ts8c6h/As"] 11 | # game_messages = ["MATCHSTATE:0:0:r200:Ad9h|", "MATCHSTATE:0:0:r200c/:Ad9h|/Ac9s9d", "MATCHSTATE:0:0:r200c/cc/:Ad9h|/Ac9s9d/6s"] 12 | 13 | 14 | def replay(): 15 | for message in game_messages: 16 | run(message) 17 | 18 | 19 | def run(msg): 20 | global last_state 21 | global last_node 22 | 23 | # parse the state message 24 | current_state, current_node = get_state(msg) 25 | 26 | # do we have a new hand? 27 | if last_state is None or last_state.hand_number != current_state.hand_number or current_node.street < last_node.street: 28 | arguments.logger.info("Starting new hand") 29 | del last_state 30 | del last_node 31 | # force clean up 32 | arguments.logger.trace( 33 | f"Initiating garbage collection. Allocated memory={torch.cuda.memory_allocated('cuda')}, Reserved memory={torch.cuda.memory_reserved('cuda')}") 34 | gc.collect() 35 | if arguments.use_gpu: 36 | torch.cuda.empty_cache() 37 | arguments.logger.trace( 38 | f"Garbage collection performed. Allocated memory={torch.cuda.memory_allocated('cuda')}, Reserved memory={torch.cuda.memory_reserved('cuda')}") 39 | continual_resolving.start_new_hand(current_state) 40 | 41 | # use continual resolving to find a strategy and make an action in the current node 42 | advised_action: protocol_to_node.Action = continual_resolving.compute_action(current_state, current_node) 43 | 44 | last_state = current_state 45 | last_node = current_node 46 | 47 | # force clean up 48 | if arguments.use_gpu: 49 | arguments.logger.trace( 50 | f"Initiating garbage collection. Allocated memory={torch.cuda.memory_allocated('cuda')}, Reserved memory={torch.cuda.memory_reserved('cuda')}") 51 | gc.collect() 52 | if arguments.use_gpu: 53 | torch.cuda.empty_cache() 54 | arguments.logger.trace(f"Garbage collection performed. Allocated memory={torch.cuda.memory_allocated('cuda')}, Reserved memory={torch.cuda.memory_reserved('cuda')}") 55 | 56 | 57 | def get_state(msg): 58 | arguments.logger.info(f"Parsing new state: {msg}") 59 | # parse the string to our state representation 60 | parsed_state = protocol_to_node.parse_state(msg) 61 | 62 | # figure out if we should act 63 | # current player to act is us 64 | if parsed_state.acting_player == parsed_state.player: 65 | # we should not act since this is an allin situations 66 | if parsed_state.bet1 == parsed_state.bet2 and parsed_state.bet1 == game_settings.stack: 67 | arguments.logger.debug("State parsed -> not our turn -or- all in") 68 | # we should act 69 | else: 70 | arguments.logger.debug("State parsed -> our turn >>>") 71 | # create a tree node from the current state 72 | node = protocol_to_node.parsed_state_to_node(parsed_state) 73 | return parsed_state, node 74 | # current player to act is the opponent 75 | else: 76 | arguments.logger.debug("State parsed -> not our turn...") 77 | 78 | 79 | if __name__ == "__main__": 80 | import gc 81 | 82 | import torch 83 | 84 | import settings.arguments as arguments 85 | import settings.game_settings as game_settings 86 | 87 | import server.protocol_to_node as protocol_to_node 88 | from lookahead.continual_resolving import ContinualResolving 89 | 90 | import utils.pseudo_random as random_ 91 | 92 | continual_resolving = ContinualResolving() 93 | 94 | arguments.logger.info("Running test") 95 | random_.manual_seed(0) 96 | replay() 97 | arguments.logger.success("Test completed") 98 | -------------------------------------------------------------------------------- /src/player/dyypholdem_acpc_player.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import argparse 4 | sys.path.append(os.getcwd()) 5 | 6 | 7 | last_state = None 8 | last_node = None 9 | 10 | 11 | def run(server, port): 12 | global last_state 13 | global last_node 14 | 15 | # 1.0 connecting to the server 16 | acpc_game = ACPCGame() 17 | acpc_game.connect(server, port) 18 | 19 | current_state: protocol_to_node.ProcessedState 20 | current_node: TreeNode 21 | 22 | winnings = 0 23 | 24 | # 2.0 main loop that waits for a situation where we act and then chooses an action 25 | while True: 26 | 27 | # 2.1 blocks until it's our situation/turn 28 | current_state, current_node, hand_winnings = acpc_game.get_next_situation() 29 | 30 | if current_state is None: 31 | # game ended or connection to server broke 32 | break 33 | 34 | if current_node is not None: 35 | # do we have a new hand? 36 | if last_state is None or last_state.hand_number != current_state.hand_number or current_node.street < last_node.street: 37 | arguments.logger.trace( 38 | f"Initiating garbage collection. Allocated memory={torch.cuda.memory_allocated('cuda')}, Reserved memory={torch.cuda.memory_reserved('cuda')}") 39 | del last_node 40 | del last_state 41 | gc.collect() 42 | if arguments.use_gpu: 43 | torch.cuda.empty_cache() 44 | arguments.logger.trace( 45 | f"Garbage collection completed. Allocated memory={torch.cuda.memory_allocated('cuda')}, Reserved memory={torch.cuda.memory_reserved('cuda')}") 46 | continual_resolving.start_new_hand(current_state) 47 | 48 | # 2.1 use continual resolving to find a strategy and make an action in the current node 49 | advised_action: protocol_to_node.Action = continual_resolving.compute_action(current_state, current_node) 50 | 51 | if advised_action.action == constants.ACPCActions.ccall: 52 | advised_action.raise_amount = abs(current_state.bet1 - current_state.bet2) 53 | 54 | # 2.2 send the action to the dealer 55 | acpc_game.play_action(advised_action) 56 | 57 | last_state = current_state 58 | last_node = current_node 59 | 60 | # force clean up 61 | arguments.logger.trace( 62 | f"Initiating garbage collection. Allocated memory={torch.cuda.memory_allocated('cuda')}, Reserved memory={torch.cuda.memory_reserved('cuda')}") 63 | gc.collect() 64 | if arguments.use_gpu: 65 | torch.cuda.empty_cache() 66 | arguments.logger.trace( 67 | f"Garbage collection completed. Allocated memory={torch.cuda.memory_allocated('cuda')}, Reserved memory={torch.cuda.memory_reserved('cuda')}") 68 | else: 69 | winnings += hand_winnings 70 | arguments.logger.success(f"Hand completed. Hand winnings: {hand_winnings}, Total winnings: {winnings}") 71 | 72 | arguments.logger.success(f"Game ended >>> Total winnings: {winnings}") 73 | 74 | 75 | if __name__ == "__main__": 76 | parser = argparse.ArgumentParser(description='Play poker on an ACPC server') 77 | parser.add_argument('hostname', type=str, help="Hostname/IP of the server running ACPC dealer") 78 | parser.add_argument('port', type=int, help="Port to connect on the ACPC server") 79 | args = parser.parse_args() 80 | 81 | import gc 82 | 83 | import torch 84 | 85 | import settings.arguments as arguments 86 | import settings.constants as constants 87 | 88 | from server.acpc_game import ACPCGame 89 | import server.protocol_to_node as protocol_to_node 90 | from tree.tree_node import TreeNode 91 | from lookahead.continual_resolving import ContinualResolving 92 | 93 | import utils.pseudo_random as random_ 94 | 95 | continual_resolving = ContinualResolving() 96 | 97 | if arguments.use_pseudo_random: 98 | random_.manual_seed(0) 99 | 100 | run(args.hostname, args.port) 101 | -------------------------------------------------------------------------------- /src/tests/ranges/situation-p1.txt: -------------------------------------------------------------------------------- 1 | 7hjh 1.000 2 | 6h7h 1.000 3 | 4s7s 1.000 4 | 9d9c 1.000 5 | 9s9d 1.000 6 | 6s7s 1.000 7 | tsqs 1.000 8 | 9s9h 1.000 9 | 9s9c 1.000 10 | 4h7h 1.000 11 | 9h9c 1.000 12 | 9h9d 1.000 13 | 9sks 1.000 14 | jhjc 1.000 15 | jdjc 1.000 16 | jhjd 1.000 17 | qsqh 1.000 18 | qsqc 1.000 19 | qhqc 1.000 20 | 6sks 1 21 | 9sqs 1 22 | 7hth 1 23 | tsth 1 24 | thtd 1 25 | tdtc 1 26 | thtc 1 27 | 7s9h 1 28 | 7h9s 1 29 | 7s9c 1 30 | 7h9c 1 31 | 7h9d 1 32 | 7s9d 1 33 | 7s9s 0.999 34 | 7h9h 0.999 35 | 6c7s 0.999 36 | 6s7h 0.999 37 | 6d7h 0.999 38 | 6c7h 0.999 39 | 6h7s 0.999 40 | 6d7s 0.999 41 | 7sas 0.999 42 | 4s6s 0.999 43 | 4c6c 0.999 44 | 4h6h 0.999 45 | 4d6d 0.999 46 | 6h9h 0.999 47 | 7sqh 0.999 48 | 7sjd 0.999 49 | 7sth 0.999 50 | 7htc 0.999 51 | 7sjh 0.999 52 | 7std 0.999 53 | 7hjd 0.999 54 | 7sjc 0.999 55 | 7stc 0.999 56 | 7hjc 0.999 57 | 7htd 0.999 58 | 6s9h 0.999 59 | 6d9s 0.999 60 | 6s9d 0.999 61 | 6h9s 0.999 62 | 6c9s 0.999 63 | 6s9c 0.999 64 | 4d6s 0.999 65 | 4c6h 0.999 66 | 4d6c 0.999 67 | 4d6h 0.999 68 | 4h6d 0.999 69 | 4h6s 0.999 70 | 4h6c 0.999 71 | 4c6d 0.999 72 | 4c6s 0.999 73 | 3h7h 0.997 74 | jsjh 0.996 75 | 2h7h 0.996 76 | 2s7s 0.996 77 | 4s6d 0.990 78 | 4s6c 0.990 79 | 6h9c 0.986 80 | 6h9d 0.986 81 | 6c9h 0.985 82 | 6d9h 0.985 83 | 8hks 0.979 84 | tstc 0.978 85 | tstd 0.978 86 | 6c9d 0.977 87 | 6d9c 0.977 88 | jsjd 0.976 89 | jsjc 0.976 90 | 8cah 0.976 91 | 8dah 0.976 92 | 4s6h 0.972 93 | 6d9d 0.968 94 | 6c9c 0.968 95 | 6s6h 0.966 96 | 7sts 0.965 97 | 7sqs 0.965 98 | 8hac 0.959 99 | 8had 0.959 100 | 5h6h 0.942 101 | 8h9s 0.939 102 | 6sts 0.936 103 | 8das 0.935 104 | 8cas 0.935 105 | 6h8d 0.933 106 | 6h8c 0.933 107 | 8dad 0.929 108 | 8cac 0.929 109 | 6d8d 0.927 110 | 6c8c 0.927 111 | 7sjs 0.927 112 | 6d8c 0.926 113 | 6c8d 0.926 114 | 8has 0.924 115 | 6hqs 0.919 116 | 8dac 0.912 117 | 8cad 0.912 118 | 6hth 0.911 119 | 7sqc 0.908 120 | 6sas 0.904 121 | 6h8h 0.901 122 | 5h6s 0.900 123 | 3s7s 0.896 124 | 7hqh 0.894 125 | 6sqs 0.894 126 | 8hah 0.893 127 | 7sks 0.889 128 | 8hkh 0.884 129 | 7hqc 0.880 130 | 7hts 0.886 131 | kskh 0.863 132 | 6c8h 0.862 133 | 6d8h 0.862 134 | 6skh 0.82 135 | 7hkc 0.817 136 | 7hkd 0.817 137 | 7hjs 0.794 138 | 7hqs 0.788 139 | 7hkh 0.772 140 | 7skd 0.758 141 | 7skc 0.758 142 | 6cqs 0.757 143 | 6dqs 0.757 144 | 6dtd 0.753 145 | 6ctc 0.753 146 | 9hjh 0.742 147 | 4s9s 0.741 148 | 6s8d 0.722 149 | 6s8c 0.722 150 | 6s6d 0.719 151 | 6s6c 0.719 152 | 7skh 0.706 153 | 6s8h 0.701 154 | 9sts 0.697 155 | 9cjc 0.680 156 | 9djd 0.68 157 | 7hks 0.663 158 | 5h7s 0.653 159 | 5h7h 0.650 160 | 6sjs 0.643 161 | 6cjc 0.635 162 | 6djd 0.635 163 | 6hjh 0.623 164 | 5d7s 0.615 165 | 5c7s 0.615 166 | 5h6d 0.615 167 | 5h6c 0.615 168 | khkc 0.614 169 | khkd 0.614 170 | 7hac 0.602 171 | 7had 0.602 172 | 5d7h 0.599 173 | 5c7h 0.599 174 | 7hah 0.587 175 | 7has 0.568 176 | 9dtd 0.551 177 | 9ctc 0.551 178 | 8cks 0.532 179 | 8dks 0.532 180 | 6skc 0.517 181 | 6skd 0.517 182 | 2s3s 0.512 183 | 6hqh 0.510 184 | 8h9h 0.51 185 | 9hth 0.506 186 | 4sks 0.481 187 | 3sqs 0.465 188 | 9hqh 0.456 189 | 7sad 0.448 190 | 7sac 0.448 191 | 6hks 0.446 192 | 5c6c 0.444 193 | 5d6d 0.444 194 | 5c6h 0.440 195 | 5d6h 0.440 196 | tsjs 0.439 197 | 7sah 0.428 198 | 3c6c 0.412 199 | 3d6d 0.412 200 | 7s7h 0.366 201 | 7h8h 0.350 202 | 7h8c 0.343 203 | 7h8d 0.343 204 | 6h6d 0.342 205 | 6h6c 0.342 206 | 9hqs 0.335 207 | 7s8c 0.333 208 | 7s8d 0.333 209 | 2s6s 0.327 210 | 8h8d 0.326 211 | 8h8c 0.326 212 | kskc 0.311 213 | kskd 0.311 214 | 7s8h 0.307 215 | 6cqc 0.293 216 | 6s9s 0.246 217 | 6hjs 0.229 218 | 6hqc 0.226 219 | 6sqc 0.222 220 | 3s9s 0.222 221 | 6d6c 0.216 222 | 9sqc 0.209 223 | 6sqh 0.193 224 | 5c6s 0.185 225 | 5d6s 0.185 226 | 9sjh 0.175 227 | 9dts 0.163 228 | 9cts 0.163 229 | 9cqc 0.160 230 | 9ctd 0.157 231 | 9dtc 0.157 232 | 9sqh 0.142 233 | 9htc 0.132 234 | 9sjd 0.111 235 | 9sjc 0.111 236 | 3s4s 0.109 237 | 9cjs 0.102 238 | 9djs 0.102 239 | 9hjs 0.092 240 | 9hjd 0.088 241 | 9hjc 0.088 242 | 3s6s 0.074 243 | 2s9s 0.074 244 | 9dth 0.072 245 | 9cth 0.072 246 | 6hkh 0.072 247 | 8hkc 0.068 248 | 8hkd 0.068 249 | 9hts 0.066 250 | 4c7s 0.055 251 | 4d7s 0.055 252 | 4s7h 0.055 253 | 4d7h 0.055 254 | 4h7s 0.055 255 | 4c7h 0.055 256 | 9djc 0.049 257 | 9cjd 0.049 258 | 6cjs 0.039 259 | 6djs 0.039 260 | 8d8c 0.036 261 | 2s4s 0.032 262 | 8dkh 0.024 263 | 8ckh 0.024 264 | 9sjs 0.021 265 | 6hkd 0.011 266 | 6hkc 0.011 267 | 9hqc 0.001 268 | -------------------------------------------------------------------------------- /src/settings/arguments.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import torch 4 | 5 | from utils.timer import Timer 6 | import utils.pseudo_random as pseudo_random 7 | import utils.output as output 8 | 9 | 10 | """Section Data Management and Paths""" 11 | # the directory for data files 12 | data_directory = '../data/' 13 | # folders for data per street 14 | street_folders = {0: "preflop-aux", 1: "preflop/", 2: "flop/", 3: "turn/", 4: "river/"} 15 | # names of streets for printing 16 | street_names = {1: "Pre-flop", 2: "Flop", 3: "Turn", 4: "River"} 17 | # path to the solved poker situation data used to train the neural net 18 | training_data_path = '../../data/training_samples/' 19 | # path to the models during training 20 | training_model_path = '../../data/models/' 21 | # folder for raw training files 22 | training_data_raw = 'raw/' 23 | # folder for converted / bucketed training files 24 | training_data_converted = 'bucketed/' 25 | # file patter for input files 26 | inputs_extension = ".inputs" 27 | # file extension for targets files 28 | targets_extension = ".targets" 29 | # path to the neural net models 30 | model_path = '../data/models/' 31 | # the name of a neural net file 32 | value_net_name = 'final' 33 | # the extension of a neural net file 34 | value_net_extension = '.tar' 35 | # flag whether to use sqlite database for bucketing information (otherwise use data files) 36 | use_sqlite = True 37 | 38 | 39 | """Section CFR Iterations""" 40 | # the number of iterations that DyypHoldem runs CFR for 41 | cfr_iters = 1000 42 | # the number of preliminary CFR iterations which DyypHoldem doesn't factor into the average strategy (included in cfr_iters) 43 | cfr_skip_iters = 500 44 | 45 | 46 | """Section Data Generation""" 47 | # how many poker situations are solved simultaneously during data generation 48 | gen_batch_size = 10 49 | # how many solved poker situations are generated for use as training examples 50 | gen_data_count = 100000 51 | 52 | 53 | """Section Training""" 54 | # how many poker situations are used in each neural net training batch - has to be a multiple of gen_batch_size ! 55 | train_batch_size = 1000 56 | # how many epochs to train for 57 | epoch_count = 200 58 | # how often to save the model during training 59 | save_epoch = 10 60 | # automatically save best epoch as final model 61 | save_best_epoch = True 62 | # learning rate for neural net training 63 | learning_rate = 0.001 64 | # resume training if a final model already exists 65 | resume_training = False 66 | 67 | 68 | """Section Torch""" 69 | # flag to use GPU for calculations 70 | use_gpu = True 71 | # default tensor types 72 | if not use_gpu: 73 | Tensor = torch.FloatTensor 74 | LongTensor = torch.LongTensor 75 | value_net_name = value_net_name + "_cpu" 76 | else: 77 | Tensor = torch.cuda.FloatTensor 78 | LongTensor = torch.cuda.LongTensor 79 | value_net_name = value_net_name + "_gpu" 80 | # flag to use new tensor cores on Ampere based GPUs - set to 'False' for reproducibility 81 | torch.backends.cuda.matmul.allow_tf32 = False 82 | # device name for torch 83 | device = torch.device('cpu') if not use_gpu else torch.device('cuda') 84 | 85 | 86 | """Section Random""" 87 | # flag to choose between official or internal random number generator - set to 'True' for reproducibility 88 | use_pseudo_random = False 89 | if use_pseudo_random: 90 | pseudo_random.manual_seed(123) 91 | 92 | 93 | """Section Global Objects""" 94 | # global logger 95 | use_loguru = True 96 | if use_loguru: 97 | import loguru 98 | logger = loguru.logger 99 | logger.remove(0) 100 | logger.level("LOADING", no=8, color="", icon="@") 101 | logger.level("TIMING", no=15, color="", icon="@") 102 | logger.level("TRACE", color="") 103 | log_format_stderr = "| {level: <8} | {message}" 104 | log_format_file = "{time:YYYY-MM-DD HH:mm:ss.SS} | {level: <8} | {message}" 105 | logging_level_stderr = "TRACE" 106 | logging_level_file = "TRACE" 107 | logger.add(sys.stderr, format=log_format_stderr, level=logging_level_stderr) 108 | logger.add("../../logs/dyypholdem.log", format=log_format_file, level=logging_level_file, rotation="10 MB") 109 | else: 110 | logger = output.DummyLogger("TRACE") 111 | 112 | # a global timer used to measure loading and calculation times 113 | timer = Timer(logger) 114 | 115 | logger.info("Environment setup complete - initializing...") 116 | -------------------------------------------------------------------------------- /src/nn/modules/concat_table.py: -------------------------------------------------------------------------------- 1 | 2 | import torch 3 | 4 | from nn.modules.container import Container 5 | 6 | 7 | class ConcatTable(Container): 8 | 9 | def __init__(self, ): 10 | super(ConcatTable, self).__init__() 11 | self.modules = [] 12 | self.output = [] 13 | 14 | def update_output(self, input): 15 | self.output = [module.update_output(input) for module in self.modules] 16 | return self.output 17 | 18 | def update_grad_input(self, input, gradOutput): 19 | return self._backward('update_grad_input', input, gradOutput) 20 | 21 | def backward(self, input, gradOutput, scale=1): 22 | return self._backward('backward', input, gradOutput, scale) 23 | 24 | def _backward(self, method, input, gradOutput, scale=1): 25 | is_table = isinstance(input, list) 26 | was_table = isinstance(self.gradInput, list) 27 | if is_table: 28 | for i, module in enumerate(self.modules): 29 | if method == 'update_grad_input': 30 | current_grad_input = module.update_grad_input(input, gradOutput[i]) 31 | elif method == 'backward': 32 | current_grad_input = module.backward(input, gradOutput[i], scale) 33 | else: 34 | assert False, "unknown target method" 35 | if not isinstance(current_grad_input, list): 36 | raise RuntimeError("currentGradInput is not a table!") 37 | 38 | if len(input) != len(current_grad_input): 39 | raise RuntimeError("table size mismatch") 40 | 41 | if i == 0: 42 | self.gradInput = self.gradInput if was_table else [] 43 | 44 | def fn(l, i, v): 45 | if i >= len(l): 46 | assert len(l) == i 47 | l.append(v.clone()) 48 | else: 49 | l[i].resize_as_(v) 50 | l[i].copy_(v) 51 | self._map_list(self.gradInput, current_grad_input, fn) 52 | else: 53 | def fn(l, i, v): 54 | if i < len(l): 55 | l[i].add_(v) 56 | else: 57 | assert len(l) == i 58 | l.append(v.clone()) 59 | self._map_list(self.gradInput, current_grad_input, fn) 60 | else: 61 | self.gradInput = self.gradInput if not was_table else input.clone() 62 | for i, module in enumerate(self.modules): 63 | if method == 'update_grad_input': 64 | current_grad_input = module.updateGradInput(input, gradOutput[i]) 65 | elif method == 'backward': 66 | current_grad_input = module.backward(input, gradOutput[i], scale) 67 | else: 68 | assert False, "unknown target method" 69 | if i == 0: 70 | self.gradInput.resize_as_(current_grad_input).copy_(current_grad_input) 71 | else: 72 | self.gradInput.add_(current_grad_input) 73 | 74 | return self.gradInput 75 | 76 | def _map_list(self, l1, l2, f): 77 | for i, v in enumerate(l2): 78 | if isinstance(v, list): 79 | res = self._map_list(l1[i] if i < len(l1) else [], v, f) 80 | if i >= len(l1): 81 | assert i == len(l1) 82 | l1.append(res) 83 | else: 84 | l1[i] = res 85 | else: 86 | f(l1, i, v) 87 | for i in range(len(l1) - 1, len(l2) - 1, -1): 88 | del l1[i] 89 | return l1 90 | 91 | def __repr__(self): 92 | tab = ' ' 93 | line = '\n' 94 | next = ' |`-> ' 95 | ext = ' | ' 96 | extlast = ' ' 97 | last = ' +. -> ' 98 | res = torch.typename(self) 99 | res = res + ' {' + line + tab + 'input' 100 | for i in range(len(self.modules)): 101 | if i == len(self.modules) - 1: 102 | res = res + line + tab + next + '(' + str(i) + '): ' + \ 103 | str(self.modules[i]).replace(line, line + tab + extlast) 104 | else: 105 | res = res + line + tab + next + '(' + str(i) + '): ' + \ 106 | str(self.modules[i]).replace(line, line + tab + ext) 107 | 108 | res = res + line + tab + last + 'output' 109 | res = res + line + '}' 110 | return res 111 | -------------------------------------------------------------------------------- /src/nn/value_nn.py: -------------------------------------------------------------------------------- 1 | 2 | import torch 3 | 4 | import settings.arguments as arguments 5 | 6 | import nn.modules.module 7 | 8 | 9 | class ValueNn(object): 10 | 11 | model_info: dict 12 | model_state: dict 13 | model: nn.modules.module 14 | 15 | def __init__(self): 16 | self.model_info = {} 17 | self.model_state = {} 18 | 19 | def __repr__(self): 20 | repr_str = "Model info: " 21 | repr_str = repr_str + f"street={self.model_info['street']}, epoch={self.model_info['epoch']}, validation loss={self.model_info['valid_loss']:0.6f}, " 22 | repr_str = repr_str + f"device={'cpu' if self.model_info['device'] == torch.device('cpu') else 'cuda'}, " 23 | repr_str = repr_str + f"datatype={'float32' if self.model_info['datatype'] is torch.float32 else 'float64'}" 24 | return repr_str 25 | 26 | # --- Gives the neural net output for a batch of inputs. 27 | # -- @param inputs An NxI tensor containing N instances of neural net inputs. 28 | # -- See @{net_builder} for details of each input. 29 | # -- @param output An NxO tensor in which to store N sets of neural net outputs. 30 | # -- See @{net_builder} for details of each output. 31 | def get_value(self, inputs, output): 32 | output.copy_(self.model.forward(inputs)) 33 | 34 | def load_for_street(self, street, aux=False, training=False): 35 | if training: 36 | net_file = arguments.training_model_path 37 | else: 38 | # load final model for specific street 39 | net_file = arguments.model_path 40 | if aux: 41 | assert street == 1 42 | net_file = net_file + "preflop-aux/" 43 | else: 44 | net_file += arguments.street_folders[street + 1] 45 | net_file = net_file + arguments.value_net_name 46 | net_file = net_file + ".tar" 47 | 48 | return self.load_from_file(net_file) 49 | 50 | def load_from_file(self, file_name: str): 51 | 52 | arguments.timer.split_start(f"Loading neural network '{file_name}'", log_level="DEBUG") 53 | 54 | saved_dict = torch.load(file_name) 55 | self.__dict__.update(saved_dict) 56 | 57 | assert self.model, "no model found in file" 58 | assert self.model_info, "no model info found in file" 59 | 60 | arguments.logger.trace(repr(self)) 61 | device = self.model_info['device'] 62 | if device: 63 | if arguments.device != device: 64 | if device == torch.device('cpu'): 65 | arguments.logger.info("Moving model trained on CPU to GPU") 66 | self.model.cuda() 67 | elif device == torch.device('cuda'): 68 | arguments.logger.info("Moving model trained on GPU to CPU") 69 | self.model.cpu() 70 | else: 71 | raise ValueError("unknown device") 72 | else: 73 | arguments.logger.warning(f"Model does not contain device information - setting it to {repr(arguments.device)}") 74 | if arguments.device == torch.device('cpu'): 75 | self.model.cpu() 76 | else: 77 | self.model.cuda() 78 | 79 | # setting model to evaluation mode 80 | self.model.evaluate() 81 | 82 | arguments.timer.split_stop("Network loaded in", log_level="LOADING") 83 | 84 | return self 85 | 86 | def load_info_from_file(self, file_name: str): 87 | 88 | arguments.timer.split_start(f"Loading neural network '{file_name}'", log_level="DEBUG") 89 | 90 | saved_dict = torch.load(file_name) 91 | self.__dict__.update(saved_dict) 92 | 93 | assert self.model, "no model found in file" 94 | assert self.model_info, "no model info found in file" 95 | 96 | arguments.timer.split_stop("Network loaded in", log_level="LOADING") 97 | 98 | return self 99 | 100 | def save_model(self, model, file_name, state=None, **kwargs): 101 | 102 | for key in kwargs: 103 | self.model_info[key] = kwargs[key] 104 | if 'device' not in self.model_info.keys(): 105 | self.model_info['device'] = arguments.device 106 | if 'datatype' not in self.model_info.keys(): 107 | self.model_info['datatype'] = torch.float32 108 | 109 | self.model = model 110 | 111 | if state is not None: 112 | self.model_state = state 113 | 114 | arguments.logger.info(f"Saving model '{file_name}'") 115 | arguments.logger.debug(f"{repr(self)}") 116 | torch.save({'model_info': self.model_info, 'model': self.model, 'model_state': self.model_state}, file_name) 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /src/nn/modules/utils.py: -------------------------------------------------------------------------------- 1 | 2 | import torch 3 | 4 | # tensorCache maintains a list of all tensors and storages that have been 5 | # converted (recursively) by calls to recursiveType() and type(). 6 | # It caches conversions in order to preserve sharing semantics 7 | # i.e. if two tensors share a common storage, then type conversion 8 | # should preserve that. 9 | # 10 | # You can preserve sharing semantics across multiple networks by 11 | # passing tensorCache between the calls to type, e.g. 12 | # 13 | # > tensorCache = {} 14 | # > net1:type('torch.cuda.FloatTensor', tensorCache) 15 | # > net2:type('torch.cuda.FloatTensor', tensorCache) 16 | # > nn.utils.recursiveType(anotherTensor, 'torch.cuda.FloatTensor', tensorCache) 17 | 18 | 19 | def recursive_type(param, type, tensorCache={}): 20 | from .criterion import Criterion 21 | from .module import Module 22 | if isinstance(param, list): 23 | for i, p in enumerate(param): 24 | param[i] = recursive_type(p, type, tensorCache) 25 | elif isinstance(param, Module) or isinstance(param, Criterion): 26 | param.type(type, tensorCache) 27 | elif isinstance(param, torch.Tensor): 28 | if param.type() != type: 29 | key = param._cdata 30 | if key in tensorCache: 31 | newparam = tensorCache[key] 32 | else: 33 | newparam = torch.Tensor(param.shape).type(type) 34 | newparam.copy_(param) 35 | tensorCache[key] = newparam 36 | param = newparam 37 | return param 38 | 39 | 40 | def recursive_resize_as(t1, t2): 41 | if isinstance(t2, list): 42 | t1 = t1 if isinstance(t1, list) else [t1] 43 | if len(t1) < len(t2): 44 | t1 += [None] * (len(t2) - len(t1)) 45 | for i, _ in enumerate(t2): 46 | t1[i], t2[i] = recursive_resize_as(t1[i], t2[i]) 47 | t1 = t1[:len(t2)] 48 | elif isinstance(t2, torch.Tensor): 49 | t1 = t1 if isinstance(t1, torch.Tensor) else t2.new() 50 | t1.resize_as_(t2) 51 | else: 52 | raise RuntimeError("Expecting nested tensors or tables. Got " + 53 | type(t1).__name__ + " and " + type(t2).__name__ + "instead") 54 | return t1, t2 55 | 56 | 57 | def recursive_fill(t2, val): 58 | from .module import Module 59 | if isinstance(t2, list): 60 | t2 = [recursive_fill(x, val) for x in t2] 61 | elif isinstance(t2, Module): 62 | t2.fill(val) 63 | elif isinstance(t2, torch.Tensor): 64 | t2.fill_(val) 65 | return t2 66 | 67 | 68 | def recursive_add(t1, val=1, t2=None): 69 | if t2 is None: 70 | t2 = val 71 | val = 1 72 | if isinstance(t2, list): 73 | t1 = t1 if isinstance(t1, list) else [t1] 74 | for i, _ in enumerate(t2): 75 | t1[i], t2[i] = recursive_add(t1[i], val, t2[i]) 76 | elif isinstance(t1, torch.Tensor) and isinstance(t2, torch.Tensor): 77 | t1.add_(val, t2) 78 | else: 79 | raise RuntimeError("expecting nested tensors or tables. Got " + 80 | type(t1).__name__ + " and " + type(t2).__name__ + " instead") 81 | return t1, t2 82 | 83 | 84 | def recursive_copy(t1, t2): 85 | if isinstance(t2, list): 86 | t1 = t1 if isinstance(t1, list) else [t1] 87 | for i, _ in enumerate(t2): 88 | t1[i], t2[i] = recursive_copy(t1[i], t2[i]) 89 | elif isinstance(t2, torch.Tensor): 90 | t1 = t1 if isinstance(t1, torch.Tensor) else t2.new() 91 | t1.resize_as_(t2).copy_(t2) 92 | else: 93 | raise RuntimeError("expecting nested tensors or tables. Got " + 94 | type(t1).__name__ + " and " + type(t2).__name__ + " instead") 95 | return t1, t2 96 | 97 | 98 | def contiguous_view(output, input, *args): 99 | if output is None: 100 | output = input.new() 101 | if input.is_contiguous(): 102 | output.set_(input.view(*args)) 103 | else: 104 | output.resize_as_(input) 105 | output.copy_(input) 106 | output.set_(output.view(*args)) 107 | return output 108 | 109 | 110 | # go over specified fields and clear them. accepts 111 | # nn.clear_state(self, ['_buffer', '_buffer2']) and 112 | # nn.clear_state(self, '_buffer', '_buffer2') 113 | def clear(self, *args): 114 | if len(args) == 1 and isinstance(args[0], list): 115 | args = args[0] 116 | 117 | def _clear(f): 118 | if not hasattr(self, f): 119 | return 120 | attr = getattr(self, f) 121 | if isinstance(attr, torch.Tensor): 122 | attr.set_() 123 | elif isinstance(attr, list): 124 | del attr[:] 125 | else: 126 | setattr(self, f, None) 127 | for key in args: 128 | _clear(key) 129 | return self 130 | -------------------------------------------------------------------------------- /src/tests/ranges/situation2-p1.txt: -------------------------------------------------------------------------------- 1 | TsAs 0.012875 2 | QcAc 0.192686 3 | QdAs 0.177237 4 | QcAs 0.177237 5 | QcAd 0.184158 6 | QdAc 0.184158 7 | QdAd 0.192686 8 | JsAs 0.005649 9 | KcAc 0.138829 10 | KsAd 0.009415 11 | KsAc 0.009415 12 | KcAs 0.052031 13 | KcAd 0.137483 14 | KsAs 0.170011 15 | AsAd 0.208837 16 | AsAc 0.208837 17 | AdAc 0.199488 18 | QhAc 0.170189 19 | QhAd 0.170189 20 | QhAs 0.157085 21 | QdQc 0.052385 22 | 5dAd 0.017711 23 | 5cAc 0.017711 24 | JcAc 0.008799 25 | JdAd 0.008799 26 | JcAd 0.007662 27 | JdAc 0.007662 28 | 5sAs 0.001169 29 | TsJs 0.042813 30 | KhAs 0.085118 31 | KhAd 0.115602 32 | KhAc 0.115602 33 | JhAs 0.001147 34 | TdJd 0.010486 35 | TcJc 0.010486 36 | JdAs 0.005942 37 | JcAs 0.005942 38 | QhQd 0.202023 39 | QhQc 0.202023 40 | 8h9h 0.041717 41 | 9hTh 0.001733 42 | 3c4c 0.026533 43 | 3d4d 0.026533 44 | 7h8h 0.03391 45 | 8hTh 0.018794 46 | 5d5c 0.052883 47 | 6h8h 0.027873 48 | 6h7h 0.04121 49 | 5s5c 0.076916 50 | 5s5d 0.076916 51 | 3s4s 0.059114 52 | ThAc 0.030011 53 | ThAd 0.030011 54 | 7h9h 0.029347 55 | 4h6h 0.022629 56 | 3h4h 0.009688 57 | 6h9h 0.024147 58 | TdJc 0.007639 59 | TcJd 0.007639 60 | 7hTh 0.034049 61 | TdJh 0.055531 62 | TcJh 0.055531 63 | ThAs 0.071851 64 | 4h7h 0.012634 65 | TdQd 0.000673 66 | TcQc 0.000673 67 | 5sAc 0.064845 68 | 5sAd 0.064845 69 | TdAd 0.079246 70 | TcAc 0.079246 71 | 4hTh 0.043931 72 | ThJd 0.03863 73 | ThJc 0.03863 74 | 5dAs 0.072294 75 | 5cAs 0.072294 76 | TsJh 0.038892 77 | 3hJh 0.069059 78 | 5dAc 0.013776 79 | 5cAd 0.013776 80 | 6h6d 0.009231 81 | 6h6c 0.009231 82 | 6hTh 0.030378 83 | TcAd 0.07591 84 | TdAc 0.07591 85 | JhKh 0.009611 86 | 5s7s 0.025734 87 | 5s8s 0.02286 88 | 5cQc 0.035853 89 | 5dQd 0.035853 90 | 4hJh 0.060204 91 | JsKs 0.005812 92 | 7hJh 0.008289 93 | ThJs 0.040627 94 | 6hJh 0.030884 95 | ThKh 0.000347 96 | TdQh 0.001823 97 | TcQh 0.001823 98 | 3hTh 0.037308 99 | JcKh 0.000934 100 | JdKh 0.000934 101 | 3s5s 0.024575 102 | 3h6h 0.012474 103 | 9cQh 0.004281 104 | 9dQh 0.004281 105 | 7h7c 0.002059 106 | 7h7d 0.002059 107 | TsKs 0.002045 108 | 4d5d 0.025888 109 | 4c5c 0.025888 110 | 2s2h 0.028576 111 | 5s6s 0.024846 112 | 3s3h 0.001982 113 | TdKh 0.013255 114 | TcKh 0.013255 115 | 5s9s 0.005584 116 | 5d6d 0.004354 117 | 5c6c 0.004354 118 | TdQc 0.001049 119 | TcQd 0.001049 120 | 4h8h 0.014043 121 | 3d5d 0.03399 122 | 3c5c 0.03399 123 | 4hKh 0.018503 124 | 4s5s 0.027164 125 | 5sQc 0.033502 126 | 5sQd 0.033502 127 | 3hKh 0.072335 128 | 5sKs 0.13698 129 | 5cQd 0.035113 130 | 5dQc 0.035113 131 | 8dQd 0.003277 132 | 8cQc 0.003277 133 | 2hJh 0.041547 134 | 5dQh 0.124608 135 | 5cQh 0.124608 136 | 5sQh 0.091956 137 | 5cKc 0.048871 138 | 2hTh 0.035427 139 | 3h9h 0.029182 140 | 5dKh 0.07234 141 | 5cKh 0.07234 142 | 5dKc 0.042486 143 | 2hQh 0.124463 144 | 9hAc 0.003917 145 | 9hAd 0.003917 146 | 4h5d 0.066644 147 | 4h5c 0.066644 148 | 9dAd 0.052463 149 | 9cAc 0.052463 150 | 5sKh 0.044651 151 | 4h5s 0.03626 152 | 8dQh 0.017434 153 | 8cQh 0.017434 154 | 4h9h 0.033067 155 | 9dAc 0.037488 156 | 9cAd 0.037488 157 | 5dKs 0.008237 158 | 5cKs 0.008237 159 | 2hKc 0.008389 160 | 5s9h 0.008825 161 | 5d9h 0.008854 162 | 5c9h 0.008854 163 | 8cAc 0.011438 164 | 8dAd 0.011438 165 | 2d3d 0.011506 166 | 2c3c 0.011506 167 | 8cAs 0.013006 168 | 8dAs 0.013006 169 | 8cAd 0.013162 170 | 8dAc 0.013162 171 | 5c8h 0.014688 172 | 5d8h 0.014688 173 | 5dJd 0.016029 174 | 5cJc 0.016029 175 | 5dJs 0.00012 176 | 2hKs 0.016178 177 | 5s6h 0.000494 178 | 6dQd 0.000581 179 | 3h7h 0.016483 180 | 8cQd 0.001366 181 | 6s6h 0.01679 182 | 5d7h 0.017885 183 | 2hQd 0.002738 184 | 5c7h 0.017885 185 | 3sKh 0.017964 186 | 5cJd 0.017986 187 | 5dJc 0.017986 188 | 2h6h 0.019178 189 | 3h8h 0.019209 190 | 2c5c 0.006802 191 | 4cKh 0.01921 192 | 3cQh 0.008199 193 | 4dKh 0.01921 194 | 5sTh 0.019601 195 | 4sKh 0.019823 196 | 5c6h 0.022814 197 | 5d6h 0.022814 198 | 3dKh 0.023955 199 | 3cKh 0.023955 200 | 2h7h 0.024077 201 | 7cQh 0.026845 202 | 7dQh 0.026845 203 | 6cQh 0.028938 204 | 6dQh 0.028938 205 | 2h3h 0.0296 206 | 3hKs 0.030566 207 | 2h8h 0.031678 208 | 5sJh 0.033156 209 | 6cQc 0.000581 210 | 6cQd 0.000774 211 | 9hAs 0.001626 212 | 5d6s 0.001928 213 | 7cQd 0.002405 214 | 7dQc 0.002405 215 | 5s7h 0.002685 216 | 5sTs 0.033328 217 | 7cQc 0.003244 218 | 8s8h 0.004057 219 | 2cKh 0.033397 220 | 5cTd 0.004871 221 | 5dTd 0.005643 222 | 2dKh 0.033397 223 | 4cQh 0.006077 224 | 2d5d 0.006802 225 | 5dTh 0.034641 226 | 2d4d 0.007283 227 | 7s7h 0.008199 228 | 5cTh 0.034641 229 | 2h4h 0.035935 230 | 2h9h 0.03762 231 | 5cJh 0.040793 232 | 5dJh 0.040793 233 | 5sJs 0.044384 234 | 6dQc 0.000774 235 | 8dQc 0.001366 236 | 5c6s 0.001928 237 | 4hKs 0.002551 238 | 7dQd 0.003244 239 | 9cAs 0.045121 240 | 5dTc 0.004871 241 | 4dQh 0.006077 242 | 9dAs 0.045121 243 | 3dQh 0.008199 244 | 2s4s 0.047701 245 | 5cJs 0.00012 246 | TdAs 0.063543 247 | 2hQc 0.002738 248 | TcAs 0.063543 249 | 5cTc 0.005643 250 | 2hKh 0.065274 251 | 2s3s 0.070069 252 | 2s5s 0.070284 253 | 2c4c 0.007283 254 | 2sKh 0.109528 255 | 5s8h 0.004287 256 | -------------------------------------------------------------------------------- /src/training/raw_converter.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import argparse 4 | sys.path.append(os.getcwd()) 5 | 6 | 7 | def convert(street: int): 8 | 9 | arguments.timer.start() 10 | 11 | bucket_conversion = BucketConversion() 12 | 13 | path = arguments.training_data_path 14 | 15 | src_folder = path + arguments.street_folders[street] + arguments.training_data_raw 16 | dest_folder = path + arguments.street_folders[street] + arguments.training_data_converted 17 | 18 | good_files = {} 19 | num_files = 0 20 | input_files = {f[0:f.find(arguments.inputs_extension)] for f in os.listdir(src_folder) if 21 | f.endswith(arguments.inputs_extension)} 22 | target_files = {f[0:f.find(arguments.targets_extension)] for f in os.listdir(src_folder) if 23 | f.endswith(arguments.targets_extension)} 24 | for file in input_files: 25 | if file in target_files: 26 | good_files[file] = 1 27 | num_files += 1 28 | 29 | arguments.logger.info(f"{num_files} file pairs to be converted") 30 | bucket_count = bucketer.get_bucket_count(street) 31 | target_size = bucket_count * constants.players_count 32 | input_size = bucket_count * constants.players_count + 1 33 | 34 | num_train = math.floor(num_files * 0.9) 35 | num_valid = num_files - num_train 36 | 37 | file_idx = 0 38 | file_pattern = r"" 39 | if street == 2: 40 | file_pattern = r"[-](......)" 41 | elif street == 3: 42 | file_pattern = r"[-](........)" 43 | elif street == 4: 44 | file_pattern = r"[-](..........)" 45 | 46 | input_batch = arguments.Tensor(arguments.gen_batch_size, input_size) 47 | target_batch = arguments.Tensor(arguments.gen_batch_size, target_size) 48 | 49 | for file_base in good_files: 50 | input_name = file_base + arguments.inputs_extension 51 | target_name = file_base + arguments.targets_extension 52 | 53 | if street > 1: 54 | board = card_to_string.string_to_board(re.compile(file_pattern).search(file_base).groups()[0]) 55 | bucket_conversion.set_board(board) 56 | else: 57 | bucket_conversion.set_board(arguments.Tensor()) 58 | 59 | arguments.logger.trace(f"Loading file '{src_folder + file_base}' for conversion") 60 | raw_input_batch = torch.load(src_folder + input_name).type(arguments.Tensor) 61 | raw_target_batch = torch.load(src_folder + target_name).type(arguments.Tensor) 62 | 63 | raw_indexes = [[0, game_settings.hand_count], [game_settings.hand_count, game_settings.hand_count * 2]] 64 | bucket_indexes = [[0, bucket_count], [bucket_count, bucket_count * 2]] 65 | 66 | for player in range(0, constants.players_count): 67 | player_index = raw_indexes[player] 68 | bucket_index = bucket_indexes[player] 69 | bucket_conversion.card_range_to_bucket_range(raw_input_batch[:, player_index[0]:player_index[1]], 70 | input_batch[:, bucket_index[0]:bucket_index[1]]) 71 | 72 | for player in range(0, constants.players_count): 73 | player_index = raw_indexes[player] 74 | bucket_index = bucket_indexes[player] 75 | bucket_conversion.hand_cfvs_to_bucket_cfvs(raw_input_batch[:, player_index[0]:player_index[1]], 76 | raw_target_batch[:, player_index[0]:player_index[1]], 77 | input_batch[:, bucket_index[0]:bucket_index[1]], 78 | target_batch[:, bucket_index[0]:bucket_index[1]]) 79 | input_batch[:, -1].copy_(raw_input_batch[:, -1]) 80 | 81 | arguments.logger.trace(f"Saving converted file '{dest_folder + file_base}'") 82 | torch.save(target_batch, dest_folder + target_name) 83 | torch.save(input_batch, dest_folder + input_name) 84 | 85 | file_idx = file_idx + 1 86 | if file_idx % 100 == 0: 87 | arguments.logger.debug(f"Progress: {file_idx} of {num_files} files completed") 88 | 89 | arguments.timer.stop(f"Conversion of {file_idx} files completed:", log_level="SUCCESS") 90 | 91 | 92 | if __name__ == '__main__': 93 | parser = argparse.ArgumentParser(description='Convert raw training data into bucketed data for the specified street') 94 | parser.add_argument('street', type=int, choices=[1, 2, 3, 4], help="Street (1=pre-flop, 2=flop, 3=turn, 4=river)") 95 | args = parser.parse_args() 96 | 97 | import math 98 | import re 99 | import torch 100 | 101 | import settings.arguments as arguments 102 | import settings.constants as constants 103 | import settings.game_settings as game_settings 104 | 105 | import game.card_to_string_conversion as card_to_string 106 | from nn.bucket_conversion import BucketConversion 107 | import nn.bucketer as bucketer 108 | 109 | street_arg = int(sys.argv[1]) 110 | 111 | arguments.logger.info(f"Converting data for street: {street_arg}") 112 | 113 | convert(street_arg) 114 | -------------------------------------------------------------------------------- /acpc_server/README: -------------------------------------------------------------------------------- 1 | This README contains information about the server code for the Annual Computer 2 | Poker Competition. Please see the LICENCE file for information regarding the 3 | code's licence. 4 | 5 | ===== Software Requirements ===== 6 | 7 | This code was developed and tested for use on Unix based systems. Though it 8 | may work on other platforms, there are no guarantees. 9 | 10 | You will need standard Unix developer tools to build the software including 11 | gcc, and make. 12 | 13 | ===== Getting Started ===== 14 | 15 | * Building the code 16 | 17 | The Makefile provides instructions for compiling the code. Running 'make' from 18 | the command line will compile the required programs. 19 | 20 | 21 | * The programs 22 | 23 | dealer - Communicates with agents connected over sockets to play a game 24 | example_player - A sample player implemented in C 25 | play_match.pl - A perl script for running matches with the dealer 26 | 27 | Usage information for each of the programs is available by running the 28 | executable without any arguments. 29 | 30 | 31 | * Playing a match 32 | 33 | The fastest way to start a match is through the play_match.pl script. An 34 | example follows: 35 | 36 | $ ./play_match.pl matchName holdem.limit.2p.reverse_blinds.game 1000 0 Alice ./example_player.limit.2p.sh Bob ./example_player.limit.2p.sh 37 | 38 | After play_match.pl finishes running, there will be two output files for the 39 | dealer and two output files for each player in the game: 40 | 41 | matchName.err - The stderr from dealer including the messages sent to players 42 | matchName.log - The log for the hands played during the match 43 | matchName.playerN.std - stdout from player N 44 | matchName.playerN.err - stderr from player N 45 | 46 | Note, play_match.pl expects player executables that take exactly two arguments: 47 | the server IP followed by the port number. The executable must be specified 48 | such that it is either a path or the executable name if it can be found in your 49 | $PATH. 50 | 51 | If you need to pass specific arguments to you agent, we suggest wrapping it in 52 | another script. play_match.pl will pass any extra arguments to dealer. 53 | Matches can also be started by calling dealer and starting the players 54 | manually. More information on this is contained in the dealer section below. 55 | 56 | 57 | * dealer 58 | 59 | Running dealer will start a process that waits for other players to connect to 60 | it. After starting dealer, it will output something similar to the following: 61 | 62 | $ ./dealer matchName holdem.limit.2p.reverse_blinds.game 1000 0 Alice Bob 63 | 16177 48777 64 | # name/game/hands/seed matchName holdem.limit.2p.reverse_blinds.game 1000 0 65 | #--t_response 10000 66 | #--t_hand 600000 67 | #--t_per_hand 6000 68 | 69 | On the first line of output there should be as many numbers as there are 70 | players in the game (in this case, "16177" and "48777"). These are the ports 71 | the dealer is listening on for players. Note that these ports are specific to 72 | the positions for players in the game. 73 | 74 | Once all the players have connected to the game, the dealer will begin playing 75 | the game and outputting the messages sent to each player. After the end of the 76 | match, you should have a log file called matchName.log in the directory where 77 | dealer was started with the hands that were played. 78 | 79 | Matches can also be started by starting the dealer and connecting the 80 | executables by hand. This can be useful if you want to start your own program 81 | in a way that is difficult to script (such as running it in a debugger). 82 | 83 | 84 | ==== Game Definitions ==== 85 | 86 | The dealer takes game definition files to determine which game of poker it 87 | plays. Please see the included game definitions for some examples. The code 88 | for handling game definitions is found in game.c and game.h. 89 | 90 | Game definitions can have the following fields (case is ignored): 91 | 92 | gamedef - the starting tag for a game definition 93 | end gamedef - ending tag for a game definition 94 | stack - the stack size for each player at the start of each hand (for no-limit) 95 | blind - the size of the blinds for each player (relative to the dealer) 96 | raisesize - the size of raises on each round (for limit games) 97 | limit - specifies a limit game 98 | nolimit - specifies a no-limit game 99 | numplayers - number of players in the game 100 | numrounds - number of betting rounds per hand of the game 101 | firstplayer - the player that acts first (relative to the dealer) on each round 102 | maxraises - the maximum number of raises on each round 103 | numsuits - the number of different suits in the deck 104 | numranks - the number of different ranks in the deck 105 | numholecards - the number of private cards to deal to each player 106 | numboardcards - the number of cards revealed on each round 107 | 108 | Empty lines or lines with '#' as the very first character will be ignored 109 | 110 | If you are creating your own game definitions, please note that game.h defines 111 | some constants for maximums in games (e.g., number of rounds). These may need 112 | to be changed for games outside of the what is being run for the Annual 113 | Computer Poker Competition. 114 | -------------------------------------------------------------------------------- /src/lookahead/cfrd_gadget.py: -------------------------------------------------------------------------------- 1 | 2 | import torch 3 | 4 | import settings.arguments as arguments 5 | import settings.game_settings as game_settings 6 | import settings.constants as constants 7 | 8 | import game.card_tools as card_tools 9 | 10 | 11 | class CFRDGadget(object): 12 | 13 | # # --- Constructor 14 | # # -- @param board board card 15 | # # -- @param player_range an initial range vector for the opponent 16 | # # -- @param opponent_cfvs the opponent counterfactual values vector used for re-solving 17 | def __init__(self, board, player_range, opponent_cfvs): 18 | assert board is not None 19 | 20 | self.input_opponent_range = player_range.clone() 21 | self.input_opponent_value = opponent_cfvs.clone() 22 | 23 | self.current_opponent_values = arguments.Tensor(game_settings.hand_count) 24 | 25 | self.regret_epsilon = 1.0 / 100000000 26 | 27 | self.play_current_strategy = arguments.Tensor(game_settings.hand_count).fill_(0) 28 | self.terminate_current_strategy = arguments.Tensor(game_settings.hand_count).fill_(1) 29 | 30 | # holds achieved CFVs at each iteration so that we can compute regret 31 | self.total_values = arguments.Tensor(game_settings.hand_count) 32 | self.total_values_p2 = None 33 | 34 | self.terminate_regrets = arguments.Tensor(game_settings.hand_count).fill_(0) 35 | self.play_regrets = arguments.Tensor(game_settings.hand_count).fill_(0) 36 | 37 | self.regret_sum = None 38 | self.play_current_regret = None 39 | self.terminate_current_regret = None 40 | self.play_positive_regrets = None 41 | self.terminate_positive_regrets = None 42 | 43 | # init range mask for masking out impossible hands 44 | self.range_mask = card_tools.get_possible_hand_indexes(board) 45 | 46 | # --- Uses one iteration of the gadget game to generate an opponent range for 47 | # -- the current re-solving iteration. 48 | # -- @param current_opponent_cfvs the vector of cfvs that the opponent receives 49 | # -- with the current strategy in the re-solve game 50 | # -- @param iteration the current iteration number of re-solving 51 | # -- @return the opponent range vector for this iteration 52 | def compute_opponent_range(self, current_opponent_cfvs): 53 | 54 | play_values = current_opponent_cfvs 55 | terminate_values = self.input_opponent_value 56 | 57 | # 1.0 compute current regrets 58 | torch.mul(play_values.view(self.play_current_strategy.shape), self.play_current_strategy, out=self.total_values) 59 | self.total_values_p2 = self.total_values_p2 if self.total_values_p2 is not None else self.total_values.clone().zero_() 60 | torch.mul(terminate_values.view(self.terminate_current_strategy.shape), self.terminate_current_strategy, out=self.total_values_p2) 61 | self.total_values.add_(self.total_values_p2) 62 | 63 | self.play_current_regret = self.play_current_regret if self.play_current_regret is not None else play_values.view(self.play_current_strategy.shape).clone().zero_() 64 | self.terminate_current_regret = self.terminate_current_regret if self.terminate_current_regret is not None else self.play_current_regret.clone().zero_() 65 | 66 | self.play_current_regret.copy_(play_values.view(self.play_current_regret.shape)) 67 | self.play_current_regret.sub_(self.total_values) 68 | 69 | self.terminate_current_regret.copy_(terminate_values.view(self.terminate_current_regret.shape)) 70 | self.terminate_current_regret.sub_(self.total_values) 71 | 72 | # 1.1 cumulate regrets 73 | self.play_regrets.add_(self.play_current_regret) 74 | self.terminate_regrets.add_(self.terminate_current_regret) 75 | 76 | # 2.0 we use cfr+ in reconstruction 77 | self.terminate_regrets.clamp_(self.regret_epsilon, constants.max_number()) 78 | self.play_regrets.clamp_(self.regret_epsilon, constants.max_number()) 79 | 80 | self.play_positive_regrets = self.play_regrets 81 | self.terminate_positive_regrets = self.terminate_regrets 82 | 83 | # 3.0 regret matching 84 | self.regret_sum = self.regret_sum if self.regret_sum is not None else self.play_positive_regrets.clone().zero_() 85 | self.regret_sum.copy_(self.play_positive_regrets) 86 | self.regret_sum.add_(self.terminate_positive_regrets) 87 | 88 | self.play_current_strategy.copy_(self.play_positive_regrets) 89 | self.terminate_current_strategy.copy_(self.terminate_positive_regrets) 90 | 91 | self.play_current_strategy.div_(self.regret_sum) 92 | self.terminate_current_strategy.div_(self.regret_sum) 93 | 94 | # 4.0 for poker, the range size is larger than the allowed hands 95 | # we need to make sure reconstruction does not choose a range that is not allowed 96 | self.play_current_strategy.mul_(self.range_mask) 97 | self.terminate_current_strategy.mul_(self.range_mask) 98 | 99 | self.input_opponent_range = self.input_opponent_range if self.input_opponent_value is not None else self.play_current_strategy.clone().zero_() 100 | self.input_opponent_range.copy_(self.play_current_strategy) 101 | 102 | return self.input_opponent_range 103 | -------------------------------------------------------------------------------- /src/data_generation/range_generator.py: -------------------------------------------------------------------------------- 1 | 2 | import torch 3 | 4 | import settings.arguments as arguments 5 | import settings.game_settings as game_settings 6 | import game.card_tools as card_tools 7 | from game.evaluation.evaluator import Evaluator 8 | from terminal_equity.terminal_equity import TerminalEquity 9 | 10 | import utils.pseudo_random as random_ 11 | 12 | 13 | class RangeGenerator(object): 14 | 15 | possible_hands_count: int 16 | possible_hands_mask: torch.Tensor 17 | reverse_order: torch.Tensor 18 | reordered_range: torch.Tensor 19 | sorted_range: torch.Tensor 20 | 21 | def __init__(self): 22 | return 23 | 24 | # -- Recursively samples a section of the range vector. 25 | # -- @param cards an NxJ section of the range tensor, where N is the batch size 26 | # -- and J is the length of the range sub-vector 27 | # -- @param mass a vector of remaining probability mass for each batch member 28 | # -- @see generate_range 29 | # -- @local 30 | def _generate_recursion(self, cards_range, mass): 31 | batch_size = cards_range.shape[0] 32 | assert mass.shape[0] == batch_size 33 | 34 | # recursion stops at 1 35 | card_count = cards_range.shape[1] 36 | if card_count == 1: 37 | cards_range.copy_(mass.view_as(cards_range)) 38 | else: 39 | rand = random_.rand(batch_size) 40 | mass1 = torch.clone(mass).mul_(rand) 41 | mass1[torch.lt(mass1, 0.00001)] = 0 42 | mass1[torch.gt(mass1, 0.99999)] = 1 43 | mass2 = mass - mass1 44 | half_size = card_count / 2 45 | if half_size % 1 != 0: 46 | half_size = half_size - 0.5 47 | half_size = half_size + random_.randint(0, 1) 48 | self._generate_recursion(cards_range[:, 0:int(half_size)], mass1) 49 | self._generate_recursion(cards_range[:, int(half_size):], mass2) 50 | 51 | # --- Samples a batch of ranges with hands sorted by strength on the board. 52 | # -- @param range a NxK tensor in which to store the sampled ranges, where N is 53 | # -- the number of ranges to sample and K is the range size 54 | # -- @see generate_range 55 | # -- @local 56 | def _generate_sorted_range(self, cards_range): 57 | batch_size = cards_range.size(0) 58 | self._generate_recursion(cards_range, arguments.Tensor(batch_size).fill_(1)) 59 | 60 | # --- Samples a batch of random range vectors. 61 | # -- 62 | # -- Each vector is sampled indepently by randomly splitting the probability 63 | # -- mass between the bottom half and the top half of the range, and then 64 | # -- recursing on the two halfs. 65 | # -- 66 | # -- @{set_board} must be called first. 67 | # -- 68 | # -- @param range a NxK tensor in which to store the sampled ranges, where N is 69 | # -- the number of ranges to sample and K is the range size 70 | def generate_range(self, cards_range: torch.Tensor): 71 | batch_size = cards_range.size(0) 72 | self.sorted_range.resize_([batch_size, self.possible_hands_count]) 73 | self._generate_sorted_range(self.sorted_range) 74 | 75 | # we have to reorder the the range back to undo the sort by strength 76 | index = self.reverse_order.expand_as(self.sorted_range) 77 | self.reordered_range = self.sorted_range.gather(1, index) 78 | cards_range.zero_() 79 | cards_range.masked_scatter_(self.possible_hands_mask.expand_as(cards_range), self.reordered_range) 80 | 81 | # --- Sets the (possibly empty) board cards to sample ranges with. 82 | # -- 83 | # -- The sampled ranges will assign 0 probability to any private hands that 84 | # -- share any cards with the board. 85 | # -- 86 | # -- @param board a possibly empty vector of board cards 87 | def set_board(self, te: TerminalEquity, board): 88 | hand_strengths = arguments.Tensor(game_settings.hand_count) 89 | for i in range(0, game_settings.hand_count): 90 | hand_strengths[i] = i 91 | if board.dim() == 0: 92 | raise NotImplementedError() 93 | elif board.size(0) == 5: 94 | hand_strengths = Evaluator.batch_eval(board, None) 95 | else: 96 | hand_strengths = te.get_hand_strengths().squeeze() 97 | 98 | possible_hand_indexes = card_tools.get_possible_hand_indexes(board) 99 | self.possible_hands_count = int(possible_hand_indexes.sum(0).item()) 100 | possible_hands_mask = possible_hand_indexes.view(1, -1) 101 | self.possible_hands_mask = possible_hands_mask.bool() 102 | 103 | non_colliding_strengths = hand_strengths.masked_select(self.possible_hands_mask) 104 | order = non_colliding_strengths.sort() 105 | 106 | # hack to create same sort order even with duplicate values 107 | if arguments.use_gpu: 108 | non_colliding_strengths2 = torch.cuda.DoubleTensor(self.possible_hands_count).zero_() 109 | else: 110 | non_colliding_strengths2 = torch.DoubleTensor(self.possible_hands_count).zero_() 111 | for i in range(0, self.possible_hands_count): 112 | non_colliding_strengths2[i] = float(non_colliding_strengths[i]) - (i / 10000) 113 | order2 = non_colliding_strengths2.sort() 114 | reverse_order = order2.indices.clone().sort().indices.clone() 115 | self.reverse_order = reverse_order.view(1, -1).to(torch.long) 116 | 117 | self.reordered_range = arguments.Tensor() 118 | self.sorted_range = arguments.Tensor() 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DyypHoldem 2 | 3 | A python implementation of [DeepHoldem](https://github.com/happypepper/DeepHoldem) (which in turn is an adoption of [DeepStack](https://www.deepstack.ai/s/DeepStack.pdf) for No Limit Texas Hold'em, extended from [DeepStack-Leduc](https://github.com/lifrordi/DeepStack-Leduc)). 4 | 5 | It uses a cross-platform software stack based on Python and PyTorch and has most data for bucketing pre-calculated and stored in data files or an sqlite3 database. This reduces loading times significantly and DyypHoldem loads and reacts on a fairly modern system in reasonable time frames. 6 | 7 | 8 | 9 | ## Setup 10 | 11 | DyypHoldem runs with the following components. It has been tested on both Linux with kernel 5.10+ and Windows 10: 12 | 13 | ### Required 14 | 15 | - Python 3.8+ 16 | - [PyTorch](https://pytorch.org/) 1.10+: install a suitable package (OS, package format, compute platform) as instructed on their website. 17 | - [Git LFS](https://git-lfs.github.com/): the large data files are stored via Git LFS. Install it on your system and run the command `git lfs install`once **before** cloning this repository. 18 | 19 | ### Optional but recommended 20 | 21 | - [CUDA Toolkit](https://developer.nvidia.com/cuda-downloads) 11.3+ for GPU support (change the value in `settings/arguments.py` to `use_gpu = False` if no GPU is present) 22 | - Python [sqlite3 module](https://docs.python.org/3/library/sqlite3.html) for the large category tables on the flop, turn and river. It can be easily installed via `pip install sqlite3` in typical Python environments. The lookup tables are also included as flat files and to use them instead, set `use_sqlite = False` in `settings/arguments.py` . This leads to longer startup times, but slightly improves performance for longer running tasks, like data generation or training. 23 | - [Loguru module](https://github.com/Delgan/loguru) for extended logging. It can be installed via `pip install loguru`. If needed, the logging output can be directed to `stdout` only by setting the flag `use_loguru = False` in `settings/arguments.py`. 24 | 25 | 26 | 27 | ## Using / converting DeepHoldem models 28 | 29 | DyypHoldem comes with a set of trained counterfactual value networks, converted from ones previously provided for [DeepHoldem](https://github.com/happypepper/DeepHoldem/issues/28#issuecomment-689021950). These models are stored in `data/models/`. To re-use your own models, they can be converted into DyypHoldem's format with the following commands: 30 | 31 | ```shell 32 | cd src && python torch7/torch7_model_converter.py 33 | ``` 34 | 35 | with 36 | 37 | - ``: path to the model to be converted (both the .info and .model files are required) 38 | - ``: the street the model is for (`1`= preflop, `2`= flop, `3`= turn, `4`= river) 39 | - ``: mode the model file was saved in torch7 (`binary`or `ascii`) 40 | 41 | 42 | 43 | ## Creating your own models 44 | 45 | New models can be created in the same way as in DeepHoldem. First a set of data is generated for a street and then the value network is trained. This is repeated for the other streets, going from river to flop. For preflop the same auxiliary network as in DeepHoldem is used. 46 | 47 | Step-by-step guide for creating new models: 48 | 49 | 1. Set the parameters `gen_batch_size` and `gen_data_count` in `settings/arguments.py` to control how much data is generated - the standard batch size is `10` and the standard data count is `100000` 50 | 1. Specify the folders in `settings/arguments.py` where the training data (`training_data_path`) and the trained models (`training_model_path`) should be stored 51 | 1. Generate data via `cd src && python data_generation/main_data_generation.py 4` 52 | 2. Convert the raw data to bucketed data via `python training/raw_converter.py 4` 53 | 5. Train the model for the street via `python training/main_train.py 4` 54 | 6. Models will be generated in the path specified in step `2`. Pick the model you like best and place it inside 55 | `data/models/river` and rename it to `final_.tar`, with `` either `gpu` or `cpu` depending on your system configuration. To automatically save the model with the lowest validation, set the flag `save_best_epoch = True` in `settings/arguments.py`. 56 | 7. Repeat steps 3-6 for turn and flop by replacing `4` with `3` or `2` and placing the models under the turn and flop folders. 57 | 58 | 59 | 60 | ## Playing against DyypHoldem 61 | 62 | You can play manually against DyypHoldem via an ACPC server. Details on ACPC as well as the source code for the server can be found here: [Annual Computer Poker Competition](http://www.computerpokercompetition.org/). A pre-compiled server and run script for Linux is included. To play follow the these steps: 63 | 64 | 1. `cd acpc_server` 65 | 2. Run `./dyypholdem_match.sh ` with `` for the number of hands to be played and `` for the seed of the random number generator. By default the ports used for the players are `18901`and `18902`. 66 | 3. Open a second terminal and `cd src && python player/manual_acpc_player.py ` with the IP address of the server (e.g. `127.0.0.1` if on the same machine) as `` and `` either `18901` to play as small blind or `18902` to play as big blind. 67 | 4. Open a third terminal and `cd src && python player/dyypholdem_acpc_player.py ` with the same `` and `` the port not used for the manual player. 68 | 5. You can play against DyypHoldem using the manual player terminal. Use the following commands to control your actions: `f` = fold, `c` = check/call, `450` = raise my total pot commitment to 450 chips. 69 | 70 | 71 | 72 | ## DyypHoldem vs. Slumbot 73 | 74 | DyppHoldem also includes a player that can play against [Slumbot](https://www.slumbot.com/) using its API. 75 | 76 | 1. `cd src` 77 | 2. `python player/dyypholdem_slumbot_player.py ` 78 | 79 | Specify the number of `` you like DyypHoldem to play and enjoy the show :-). 80 | 81 | -------------------------------------------------------------------------------- /src/nn/bucketing/river_tools.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import settings.game_settings as game_settings 4 | 5 | base_values_pow7: [] 6 | base_values_pow6: [] 7 | base_values_pow5: [] 8 | base_values_pow4: [] 9 | base_values_pow3: [] 10 | base_values_pow2: [] 11 | base_values_pow1: [] 12 | 13 | 14 | def initialize(): 15 | global base_values_pow7 16 | global base_values_pow6 17 | global base_values_pow5 18 | global base_values_pow4 19 | global base_values_pow3 20 | global base_values_pow2 21 | global base_values_pow1 22 | 23 | base_values_pow7 = [0] * game_settings.card_count 24 | base_values_pow6 = [0] * game_settings.card_count 25 | base_values_pow5 = [0] * game_settings.card_count 26 | base_values_pow4 = [0] * game_settings.card_count 27 | base_values_pow3 = [0] * game_settings.card_count 28 | base_values_pow2 = [0] * game_settings.card_count 29 | base_values_pow1 = [0] * game_settings.card_count 30 | 31 | for i in range(game_settings.card_count): 32 | base_values_pow7[i] = math.floor(i / 4) * 13 * 13 * 13 * 13 * 13 * 13 33 | base_values_pow6[i] = math.floor(i / 4) * 13 * 13 * 13 * 13 * 13 34 | base_values_pow5[i] = math.floor(i / 4) * 13 * 13 * 13 * 13 35 | base_values_pow4[i] = math.floor(i / 4) * 13 * 13 * 13 36 | base_values_pow3[i] = math.floor(i / 4) * 13 * 13 37 | base_values_pow2[i] = math.floor(i / 4) * 13 38 | base_values_pow1[i] = math.floor(i / 4) 39 | 40 | 41 | initialize() 42 | 43 | 44 | def suitcat_river(s1, s2, s3, s4, s5, s6, s7): 45 | suit = {0: 0, 1: 0, 2: 0, 3: 0} 46 | suit[s3] = suit[s3] + 1 47 | suit[s4] = suit[s4] + 1 48 | suit[s5] = suit[s5] + 1 49 | suit[s6] = suit[s6] + 1 50 | suit[s7] = suit[s7] + 1 51 | 52 | if suit[0] <= 2 and suit[1] <= 2 and suit[2] <= 2 and suit[3] <= 2: 53 | return 0 54 | 55 | if suit[0] == 3 or suit[1] == 3 or suit[2] == 3 or suit[3] == 3: 56 | the_suit = -1 57 | for i in range(0, 4): 58 | if suit[i] == 3: 59 | the_suit = i 60 | mask = 0 61 | if s3 == the_suit: mask = mask + 1 62 | if s4 == the_suit: mask = mask + 2 63 | if s5 == the_suit: mask = mask + 4 64 | if s6 == the_suit: mask = mask + 8 65 | if s7 == the_suit: mask = mask + 16 66 | 67 | add = 0 68 | if s1 == the_suit and s2 == the_suit: 69 | add = 1 70 | elif s1 == the_suit: 71 | add = 2 72 | elif s2 == the_suit: 73 | add = 3 74 | 75 | if mask == 7: 76 | return 1 + add 77 | elif mask == 11: 78 | return 5 + add 79 | elif mask == 19: 80 | return 9 + add 81 | elif mask == 13: 82 | return 13 + add 83 | elif mask == 21: 84 | return 17 + add 85 | elif mask == 25: 86 | return 21 + add 87 | elif mask == 14: 88 | return 25 + add 89 | elif mask == 22: 90 | return 29 + add 91 | elif mask == 26: 92 | return 33 + add 93 | elif mask == 28: 94 | return 37 + add 95 | 96 | raise ValueError("bad river suits") 97 | 98 | if suit[0] == 4 or suit[1] == 4 or suit[2] == 4 or suit[3] == 4: 99 | the_suit = -1 100 | for i in range(0, 4): 101 | if suit[i] == 4: 102 | the_suit = i 103 | if s3 != the_suit: 104 | if s1 == the_suit and s2 == the_suit: return 42 105 | if s1 == the_suit: return 43 106 | if s2 == the_suit: return 44 107 | return 45 108 | elif s4 != the_suit: 109 | if s1 == the_suit and s2 == the_suit: return 46 110 | if s1 == the_suit: return 47 111 | if s2 == the_suit: return 48 112 | return 49 113 | elif s5 != the_suit: 114 | if s1 == the_suit and s2 == the_suit: return 50 115 | if s1 == the_suit: return 51 116 | if s2 == the_suit: return 52 117 | return 53 118 | elif s6 != the_suit: 119 | if s1 == the_suit and s2 == the_suit: return 54 120 | if s1 == the_suit: return 55 121 | if s2 == the_suit: return 56 122 | return 57 123 | elif s7 != the_suit: 124 | if s1 == the_suit and s2 == the_suit: return 58 125 | if s1 == the_suit: return 59 126 | if s2 == the_suit: return 60 127 | return 61 128 | 129 | raise ValueError("bad river suits") 130 | 131 | if suit[0] == 5 or suit[1] == 5 or suit[2] == 5 or suit[3] == 5: 132 | the_suit = -1 133 | for i in range(0, 3): 134 | if suit[i] == 5: 135 | the_suit = i 136 | if s1 == the_suit and s2 == the_suit: return 62 137 | if s1 == the_suit: return 63 138 | if s2 == the_suit: return 64 139 | return 65 140 | 141 | raise ValueError("bad river suits") 142 | 143 | 144 | def river_id(hand): 145 | base_value = base(hand) 146 | suit_code = suit(hand) 147 | if suit_code == -1: 148 | raise ValueError("invalid suit code") 149 | suit_code = suit_code * 815730722 150 | 151 | return base_value + suit_code 152 | 153 | 154 | def base(hand): 155 | v1 = base_values_pow7[hand[0]] 156 | v2 = base_values_pow6[hand[1]] 157 | v3 = base_values_pow5[hand[2]] 158 | v4 = base_values_pow4[hand[3]] 159 | v5 = base_values_pow3[hand[4]] 160 | v6 = base_values_pow2[hand[5]] 161 | v7 = base_values_pow1[hand[6]] 162 | return v1 + v2 + v3 + v4 + v5 + v6 + v7 163 | 164 | 165 | def suit(hand): 166 | return suitcat_river((hand[0] + 1) % 4, 167 | (hand[1] + 1) % 4, 168 | (hand[2] + 1) % 4, 169 | (hand[3] + 1) % 4, 170 | (hand[4] + 1) % 4, 171 | (hand[5] + 1) % 4, 172 | (hand[6] + 1) % 4) 173 | 174 | -------------------------------------------------------------------------------- /src/training/data_stream.py: -------------------------------------------------------------------------------- 1 | import os 2 | import math 3 | 4 | import torch 5 | 6 | import settings.arguments as arguments 7 | import settings.constants as constants 8 | 9 | import nn.bucketer as bucketer 10 | 11 | import utils.pseudo_random as random_ 12 | 13 | 14 | class DataStream(object): 15 | 16 | def __init__(self, street): 17 | 18 | path = arguments.training_data_path 19 | 20 | self.src_folder = path + arguments.street_folders[street] + arguments.training_data_converted 21 | 22 | self.good_files = [] 23 | num_files = 0 24 | input_files = {f[0:f.find(arguments.inputs_extension)] for f in os.listdir(self.src_folder) if 25 | f.endswith(arguments.inputs_extension)} 26 | target_files = {f[0:f.find(arguments.targets_extension)] for f in os.listdir(self.src_folder) if 27 | f.endswith(arguments.targets_extension)} 28 | for file in input_files: 29 | if file in target_files: 30 | self.good_files.append(file) 31 | num_files += 1 32 | arguments.logger.debug(f"{num_files} good files found for training") 33 | 34 | self.bucket_count = bucketer.get_bucket_count(street) 35 | self.target_size = self.bucket_count * constants.players_count 36 | self.input_size = self.bucket_count * constants.players_count + 1 37 | 38 | num_train = math.floor(num_files * 0.9) 39 | num_valid = num_files - num_train 40 | 41 | train_count = num_train * arguments.gen_batch_size 42 | valid_count = num_valid * arguments.gen_batch_size 43 | 44 | self.train_data_count = train_count 45 | assert self.train_data_count >= arguments.train_batch_size, 'Training data count has to be greater than a train batch size!' 46 | self.train_batch_count = int(self.train_data_count / arguments.train_batch_size) 47 | self.valid_data_count = valid_count 48 | assert self.valid_data_count >= arguments.train_batch_size, 'Validation data count has to be greater than a train batch size!' 49 | self.valid_batch_count = int(self.valid_data_count / arguments.train_batch_size) 50 | 51 | # --- Randomizes the order of training data. 52 | # -- 53 | # -- Done so that the data is encountered in a different order for each epoch. 54 | def start_epoch(self): 55 | # data are shuffled each epoch] 56 | self.shuffle(self.good_files, int(self.train_data_count / arguments.gen_batch_size)) 57 | 58 | @staticmethod 59 | def shuffle(tbl: list, n): 60 | if arguments.use_pseudo_random: 61 | tbl.sort() 62 | return tbl 63 | else: 64 | for i in range(n, 0, -1): 65 | rand = random_.randint(1, n) 66 | tbl[i], tbl[rand] = tbl[rand], tbl[i] 67 | return tbl 68 | 69 | # --- Gives the number of batches of training data. 70 | # -- 71 | # -- Batch size is defined by @{arguments.train_batch_size} 72 | # -- @return the number of batches 73 | def get_training_batch_count(self): 74 | return self.train_batch_count 75 | 76 | # --- Gives the number of batches of validation data. 77 | # -- 78 | # -- Batch size is defined by @{arguments.train_batch_size}. 79 | # -- @return the number of batches 80 | def get_validation_batch_count(self): 81 | return self.valid_batch_count 82 | 83 | # --- Returns a batch of data from the training set. 84 | # -- @param batch_index the index of the batch to return 85 | # -- @return the inputs set for the batch 86 | # -- @return the targets set for the batch 87 | # -- @return the masks set for the batch 88 | def get_training_batch(self, batch_index): 89 | return self.get_batch(batch_index) 90 | 91 | # --- Returns a batch of data from the validation set. 92 | # -- @param batch_index the index of the batch to return 93 | # -- @return the inputs set for the batch 94 | # -- @return the targets set for the batch 95 | # -- @return the masks set for the batch 96 | def get_validation_batch(self, batch_index): 97 | return self.get_batch(self.train_batch_count + batch_index) 98 | 99 | # --- Returns a batch of data from a specified data set. 100 | # -- @param inputs the inputs set for the given data set 101 | # -- @param targets the targets set for the given data set 102 | # -- @param mask the masks set for the given data set 103 | # -- @param batch_index the index of the batch to return 104 | # -- @return the inputs set for the batch 105 | # -- @return the targets set for the batch 106 | # -- @return the masks set for the batch 107 | # -- @local 108 | def get_batch(self, batch_index) -> {torch.Tensor, torch.Tensor, torch.Tensor}: 109 | 110 | inputs = arguments.Tensor(arguments.train_batch_size, self.input_size) 111 | targets = arguments.Tensor(arguments.train_batch_size, self.target_size) 112 | masks = arguments.Tensor(arguments.train_batch_size, self.target_size).zero_() 113 | 114 | for i in range(int(arguments.train_batch_size / arguments.gen_batch_size)): 115 | 116 | idx = batch_index * arguments.train_batch_size / arguments.gen_batch_size + i 117 | idx = math.floor(idx + 0.1) 118 | file_base = self.good_files[idx] 119 | 120 | input_name = file_base + ".inputs" 121 | target_name = file_base + ".targets" 122 | 123 | input_batch = torch.load(self.src_folder + input_name) 124 | target_batch = torch.load(self.src_folder + target_name) 125 | 126 | data_index = [i * arguments.gen_batch_size, (i + 1) * arguments.gen_batch_size] 127 | 128 | inputs[data_index[0]:data_index[1], :].copy_(input_batch) 129 | targets[data_index[0]:data_index[1], :].copy_(target_batch) 130 | masks[data_index[0]:data_index[1]][torch.gt(input_batch[:, 0:self.bucket_count * 2], 0)] = 1 131 | 132 | return inputs, targets, masks 133 | -------------------------------------------------------------------------------- /src/torch7/torch7_factory.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | import torch 4 | 5 | import settings.arguments as arguments 6 | 7 | 8 | class Torch7Tensor(torch.Tensor): 9 | torch_type: str 10 | dimensions: int 11 | sizes: list 12 | strides: list 13 | elements: list 14 | total_size: int = 1 15 | offset: int = 0 16 | 17 | def __new__(cls): 18 | if arguments.use_gpu: 19 | return super().__new__(cls).cuda() 20 | else: 21 | return super().__new__(cls) 22 | 23 | def __init__(self): 24 | super().__init__() 25 | 26 | def read(self, torch_file): 27 | if 'b' in torch_file.file.mode: 28 | self.read_binary(torch_file) 29 | else: 30 | self.read_ascii(torch_file) 31 | 32 | def read_ascii(self, torch_file): 33 | self.dimensions = int(torch_file.file.readline().strip()) 34 | if self.dimensions != 0: 35 | str_sizes = torch_file.file.readline().split(' ') 36 | if len(str_sizes) > 0: 37 | self.sizes = [0] * len(str_sizes) 38 | for i in range(0, len(str_sizes)): 39 | self.sizes[i] = int(str_sizes[i]) 40 | self.total_size *= self.sizes[i] 41 | str_strides = torch_file.file.readline().split(' ') 42 | if len(str_strides) > 0: 43 | self.strides = [0] * len(str_strides) 44 | for i in range(0, len(str_strides)): 45 | self.strides[i] = int(str_strides[i]) 46 | self.offset = int(torch_file.file.readline().strip()) 47 | self.offset -= 1 48 | 49 | """ assuming storage follows directly in file or is referenced """ 50 | type_idx = int(torch_file.file.readline().strip()) 51 | idx = int(torch_file.file.readline().strip()) 52 | if idx in torch_file.file_objects: 53 | ref: Torch7Tensor = torch_file.file_objects[idx] 54 | self.set_(ref.storage(), self.offset, torch.Size(self.sizes), tuple(self.strides)) 55 | else: 56 | # skip over version 57 | torch_file.file.readline() 58 | torch_file.file.readline() 59 | torch_file.file.readline() 60 | str_storage = torch_file.file.readline().strip() 61 | if str_storage == "torch.FloatStorage" or str_storage == "torch.CudaStorage": 62 | element_count = int(torch_file.file.readline().strip()) 63 | str_storage = torch_file.file.readline().split(' ') 64 | storage = [0.0] * element_count 65 | for i in range(0, element_count): 66 | storage[i] = float(str_storage[i]) 67 | 68 | temp = arguments.Tensor(storage) 69 | self.resize_(element_count) 70 | self.copy_(temp) 71 | self.as_strided_(self.sizes, self.strides, self.offset) 72 | 73 | torch_file.file_objects[idx] = self 74 | else: 75 | self.resize_(0) 76 | torch_file.file.readline() 77 | torch_file.file.readline() 78 | 79 | def read_binary(self, torch_file): 80 | self.dimensions = int(struct.unpack('i', torch_file.file.read(4))[0]) 81 | if self.dimensions != 0: 82 | self.sizes = [0] * self.dimensions 83 | for i in range(0, self.dimensions): 84 | self.sizes[i] = int(struct.unpack('q', torch_file.file.read(8))[0]) 85 | self.total_size *= self.sizes[i] 86 | self.strides = [0] * self.dimensions 87 | for i in range(0, self.dimensions): 88 | self.strides[i] = int(struct.unpack('q', torch_file.file.read(8))[0]) 89 | self.offset = int(struct.unpack('q', torch_file.file.read(8))[0]) 90 | self.offset -= 1 91 | 92 | """ assuming storage follows directly in file or is referenced """ 93 | type_idx = int(struct.unpack('i', torch_file.file.read(4))[0]) 94 | idx = int(struct.unpack('i', torch_file.file.read(4))[0]) 95 | if idx in torch_file.file_objects: 96 | ref: Torch7Tensor = torch_file.file_objects[idx] 97 | self.set_(ref.storage(), self.offset, torch.Size(self.sizes), tuple(self.strides)) 98 | else: 99 | # skip over version 100 | version_size = int(struct.unpack('i', torch_file.file.read(4))[0]) 101 | torch_file.file.read(version_size) 102 | size = int(struct.unpack('i', torch_file.file.read(4))[0]) 103 | str_storage = str(torch_file.file.read(size), 'utf-8') 104 | if str_storage == "torch.FloatStorage" or str_storage == "torch.CudaStorage": 105 | element_count = int(struct.unpack('q', torch_file.file.read(8))[0]) 106 | byte_arr = torch_file.file.read(element_count*4) 107 | storage = torch.FloatStorage.from_buffer(byte_arr, byte_order="native") 108 | if arguments.use_gpu: 109 | storage = storage.cuda() 110 | self.set_(storage, self.offset, torch.Size(self.sizes), tuple(self.strides)) 111 | torch_file.file_objects[idx] = self 112 | else: 113 | self.resize_(0) 114 | torch_file.file.read(12) 115 | 116 | 117 | class FloatTensor(Torch7Tensor): 118 | 119 | def __init__(self): 120 | super().__init__() 121 | self.torch_type = "torch.FloatTensor" 122 | 123 | 124 | class CudaTensor(Torch7Tensor): 125 | 126 | def __init__(self): 127 | super().__init__() 128 | self.torch_type = "torch.CudaTensor" 129 | 130 | 131 | class TorchFactory(object): 132 | 133 | torch_types = { 134 | "torch.FloatTensor": FloatTensor, 135 | "torch.CudaTensor": CudaTensor, 136 | } 137 | 138 | def create_torch_object(self, torch_obj) -> torch.Tensor: 139 | return self.torch_types[torch_obj]() 140 | -------------------------------------------------------------------------------- /src/torch7/torch7_serialization.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | import torch 4 | 5 | import settings.arguments as arguments 6 | 7 | 8 | def deserialize_from_torch7(file_name: str) -> torch.Tensor: 9 | 10 | ret: arguments.Tensor() 11 | 12 | with open(file_name, "r") as reader: 13 | input_line: str 14 | dimensions: int 15 | sizes: list 16 | strides: list 17 | element_count: int = 0 18 | elements: list 19 | total_size: int = 1 20 | offset: int = 0 21 | 22 | line_count = 0 23 | while True: 24 | input_line = reader.readline() 25 | if not input_line: 26 | break 27 | 28 | if input_line.startswith("torch.FloatTensor"): 29 | input_line = reader.readline() 30 | dimensions = int(input_line) 31 | input_line = reader.readline() 32 | str_sizes = input_line.split(' ') 33 | if len(str_sizes) > 0: 34 | sizes = [0] * len(str_sizes) 35 | for i in range(0, len(str_sizes)): 36 | sizes[i] = int(str_sizes[i]) 37 | total_size *= sizes[i] 38 | 39 | input_line = reader.readline() 40 | str_strides = input_line.split(' ') 41 | if len(str_strides) > 0: 42 | strides = [0] * len(str_strides) 43 | for i in range(0, len(str_strides)): 44 | strides[i] = int(str_strides[i]) 45 | 46 | input_line = reader.readline() 47 | offset = int(input_line) 48 | offset -= 1 49 | 50 | if input_line.startswith("torch.FloatStorage"): 51 | input_line = reader.readline() 52 | element_count = int(input_line) 53 | if sizes: 54 | input_line = reader.readline() 55 | str_elements = input_line.split(' ') 56 | if len(str_elements) > 0 and len(str_elements) == element_count: 57 | ret = arguments.Tensor(sizes) 58 | elements = [0.0] * total_size 59 | for i in range(0, total_size): 60 | elements[i] = float(str_elements[i + offset]) 61 | ret = arguments.Tensor(elements) 62 | ret.resize_(sizes) 63 | # ret.copy_(temp.view(ret.shape)) 64 | 65 | return ret 66 | 67 | 68 | def serialize_as_torch7(file_name: str, tensor: torch.Tensor, header=True, multi_line=False): 69 | 70 | source = tensor 71 | dimension_count = str(len(source.size())) 72 | total_size: int = 1 73 | sizes: str = "" 74 | strides: str = "" 75 | for i in range(0, len(source.size())): 76 | total_size *= source.size(i) 77 | sizes += str(source.size(i)) 78 | sizes += " " 79 | strides += str(source.stride(i)) 80 | strides += " " 81 | element_count = str(total_size) 82 | 83 | with open(file_name, "w") as writer: 84 | # header 85 | if header: 86 | writer.write("4" + '\n') 87 | writer.write("1" + '\n') 88 | writer.write("3" + '\n') 89 | writer.write("V 1" + '\n') 90 | writer.write("17" + '\n') 91 | writer.write("torch.FloatTensor" + '\n') 92 | writer.write(dimension_count + '\n') 93 | writer.write(sizes.strip() + '\n') 94 | writer.write(strides.strip() + '\n') 95 | writer.write(str(source.storage_offset() + 1) + '\n') 96 | writer.write("4" + '\n') 97 | writer.write("2" + '\n') 98 | writer.write("3" + '\n') 99 | writer.write("V 1" + '\n') 100 | writer.write("18" + '\n') 101 | writer.write("torch.FloatStorage" + '\n') 102 | writer.write(element_count + '\n') 103 | 104 | # elements 105 | source = source.flatten() 106 | 107 | # write as text-formatted floats 108 | elements_formatted = ["{:.9f}".format(elem) for elem in source.tolist()] 109 | if not multi_line: 110 | elements_clean = ' '.join(["0.00000000" if elem == "-0.00000000" else elem for elem in elements_formatted]) 111 | else: 112 | elements_clean = '\n'.join(["0.00000000" if elem == "-0.00000000" else elem for elem in elements_formatted]) 113 | writer.write(elements_clean) 114 | writer.write('\n') 115 | 116 | 117 | def serialize_as_bin_torch7(file_name: str, tensor: torch.Tensor, header=True): 118 | 119 | source = tensor 120 | dimension_count = len(source.size()) 121 | total_size: int = 1 122 | for i in range(0, len(source.size())): 123 | total_size *= source.size(i) 124 | 125 | with open(file_name, "wb") as writer: 126 | # header 127 | if header: 128 | writer.write(struct.pack("i", 4)) 129 | writer.write(struct.pack("i", 1)) 130 | writer.write(struct.pack("i", 3)) 131 | writer.write("V 1".encode("ascii")) 132 | writer.write(struct.pack("i", 17)) 133 | writer.write("torch.FloatTensor".encode("ascii")) 134 | writer.write(struct.pack("i", dimension_count)) 135 | for i in range(dimension_count): 136 | writer.write(struct.pack("l", source.size(i))) 137 | for i in range(dimension_count): 138 | writer.write(struct.pack("l", source.stride(i))) 139 | writer.write(struct.pack("l", source.storage_offset() + 1)) 140 | writer.write(struct.pack("i", 4)) 141 | writer.write(struct.pack("i", 2)) 142 | writer.write(struct.pack("i", 3)) 143 | writer.write("V 1".encode("ascii")) 144 | writer.write(struct.pack("i", 18)) 145 | writer.write("torch.FloatStorage".encode("ascii")) 146 | writer.write(struct.pack("l", total_size)) 147 | 148 | # elements 149 | source = source.flatten() 150 | 151 | # write elements as bytes 152 | for i in range(total_size): 153 | writer.write(struct.pack('f', source.data[i])) 154 | -------------------------------------------------------------------------------- /src/server/acpc_game.py: -------------------------------------------------------------------------------- 1 | 2 | import settings.arguments as arguments 3 | import settings.constants as constants 4 | import settings.game_settings as game_settings 5 | 6 | from server.network_communication import ACPCNetworkCommunication 7 | import server.protocol_to_node as protocol_to_node 8 | 9 | from tree.tree_node import TreeNode 10 | from game.evaluation.evaluator import Evaluator 11 | import game.card_to_string_conversion as card_conversion 12 | 13 | 14 | class ACPCGame(object): 15 | 16 | debug_msg: str 17 | network_communication: ACPCNetworkCommunication 18 | last_msg: str 19 | 20 | def __init__(self): 21 | pass 22 | 23 | # --- Connects to a specified ACPC server which acts as the dealer. 24 | # -- 25 | # -- @param server the server that sends states to DyypHoldem, which responds with actions 26 | # -- @param port the port to connect on 27 | # -- @see network_communication.connect 28 | def connect(self, server, port): 29 | arguments.logger.debug(f"Connecting to ACPC server with IP={server} and port={port}") 30 | self.network_communication = ACPCNetworkCommunication() 31 | self.network_communication.connect(server, port) 32 | 33 | def string_to_state_node(self, msg): 34 | arguments.logger.trace(f"Parsing new state from server: {msg}") 35 | parsed_state = protocol_to_node.parse_state(msg) 36 | # current player to act is us 37 | if parsed_state.acting_player == parsed_state.position: 38 | # we should not act since this is an allin situations 39 | if parsed_state.bet1 == parsed_state.bet2 and parsed_state.bet1 == game_settings.stack: 40 | arguments.logger.debug("Not our turn -or- all in") 41 | # we should act 42 | else: 43 | arguments.logger.debug("Our turn >>>") 44 | self.last_msg = msg 45 | # create a tree node from the current state 46 | node = protocol_to_node.parsed_state_to_node(parsed_state) 47 | return parsed_state, node 48 | # current player to act is the opponent 49 | else: 50 | arguments.logger.debug("Not our turn...") 51 | return None, None 52 | 53 | # --- Receives and parses the next poker situation where DyypHoldem must act. 54 | # -- 55 | # -- Blocks until the server sends a situation where DyypHoldem acts. 56 | # -- @return the parsed state representation of the poker situation (see 57 | # -- @{protocol_to_node.parse_state}) 58 | # -- @return a public tree node for the state (see 59 | # -- @{protocol_to_node.parsed_state_to_node}) 60 | def get_next_situation(self) -> (protocol_to_node.ProcessedState, TreeNode): 61 | while True: 62 | msg = None 63 | 64 | # 1.0 get the message from the dealer 65 | msg = self.network_communication.get_line() 66 | 67 | if not msg: 68 | arguments.logger.trace("Received empty message from server -> ending game") 69 | return None, None, 0 70 | 71 | arguments.logger.info(f"Received ACPC dealer message: {msg.strip()}") 72 | 73 | # 2.0 parse the string to our state representation 74 | parsed_state = protocol_to_node.parse_state(msg) 75 | arguments.logger.debug(parsed_state) 76 | 77 | # 3.0 figure out if we should act 78 | # current player to act is us 79 | if parsed_state.acting_player == constants.Players.Chance: 80 | # hand has ended 81 | my_bet = parsed_state.bet2 if parsed_state.position == 0 else parsed_state.bet1 82 | opp_bet = parsed_state.bet1 if parsed_state.position == 0 else parsed_state.bet2 83 | if parsed_state.all_actions[-1].action is constants.ACPCActions.fold: 84 | have_won = my_bet >= opp_bet 85 | else: 86 | my_final_hand = parsed_state.my_hand_string + parsed_state.board 87 | opp_final_hand = parsed_state.opponent_hand_string + parsed_state.board 88 | my_strength = Evaluator.evaluate_seven_card_hand(card_conversion.string_to_board(my_final_hand)) 89 | opp_strength = Evaluator.evaluate_seven_card_hand(card_conversion.string_to_board(opp_final_hand)) 90 | have_won = my_strength.item() <= opp_strength.item() 91 | 92 | winner = parsed_state.player if have_won else (constants.Players(1 - parsed_state.player.value)) 93 | winnings = opp_bet if have_won else -my_bet 94 | arguments.logger.trace(f"Hand ended with winner {winner}") 95 | arguments.logger.trace(f"Final bets: {parsed_state.player}={my_bet}, {constants.Players(1 - parsed_state.player.value)}={opp_bet}") 96 | 97 | return parsed_state, None, winnings 98 | 99 | elif parsed_state.acting_player == parsed_state.player: 100 | # we should not act since this is an allin situations 101 | if parsed_state.bet1 == parsed_state.bet2 and parsed_state.bet1 == game_settings.stack: 102 | arguments.logger.debug("All in situation") 103 | # we should act 104 | else: 105 | arguments.logger.debug("Our turn >>>") 106 | self.last_msg = msg.strip() 107 | # create a tree node from the current state 108 | node = protocol_to_node.parsed_state_to_node(parsed_state) 109 | return parsed_state, node, 0 110 | # current player to act is the opponent 111 | else: 112 | arguments.logger.debug("Not our turn...") 113 | 114 | # --- Informs the server that DyypHoldem is playing a specified action. 115 | # -- @param adviced_action a table specifying the action chosen by DyypHoldem, with the fields: 116 | # -- * `action`: an element of @{constants.acpc_actions} 117 | # -- * `raise_amount`: the number of chips raised (if `action` is raise) 118 | def play_action(self, advised_action): 119 | message = protocol_to_node.action_to_message(self.last_msg, advised_action) 120 | arguments.logger.debug(f"Sending action message to the ACPC dealer: {message}") 121 | self.network_communication.send_line(message) 122 | -------------------------------------------------------------------------------- /src/tests/ranges/situation2-p2.txt: -------------------------------------------------------------------------------- 1 | ThQh 1.0 2 | TsAs 1.0 3 | QcAc 1.0 4 | QdAs 1.0 5 | QcAs 1.0 6 | QcAd 1.0 7 | QdAc 1.0 8 | QdAd 1.0 9 | JsAs 1.0 10 | KcAc 0.999988 11 | KsAd 0.99948 12 | KsAc 0.99948 13 | KcAs 0.99948 14 | KcAd 0.99948 15 | KsAs 0.999246 16 | AsAd 0.995804 17 | AsAc 0.995804 18 | AdAc 0.995165 19 | QhAc 0.985218 20 | QhAd 0.985218 21 | 9hQh 0.983855 22 | QhAs 0.983609 23 | QdQc 0.94031 24 | 5dAd 0.938396 25 | 5cAc 0.938396 26 | JcAc 0.922008 27 | JdAd 0.922008 28 | JhQh 0.91829 29 | JcAd 0.909971 30 | JdAc 0.909971 31 | 5sAs 0.892997 32 | TsJs 0.884349 33 | JsAc 0.864439 34 | JsAd 0.864439 35 | KhAs 0.861762 36 | KhAd 0.78992 37 | KhAc 0.78992 38 | JhAs 0.783567 39 | TdJd 0.77909 40 | TcJc 0.77909 41 | ThJh 0.728156 42 | JdAs 0.724391 43 | JcAs 0.724391 44 | QhQd 0.695481 45 | QhQc 0.695481 46 | 8h9h 0.690308 47 | 9sAs 0.687567 48 | 8hQh 0.682829 49 | 9hTh 0.67847 50 | JhAd 0.649819 51 | JhAc 0.649819 52 | 8sAs 0.598506 53 | 6s7s 0.586651 54 | 3c4c 0.568265 55 | 3d4d 0.568265 56 | 7h8h 0.531157 57 | 8hTh 0.524748 58 | 5d5c 0.51912 59 | 8s9s 0.508511 60 | 6h8h 0.502902 61 | 6h7h 0.49231 62 | 5s5c 0.489826 63 | 5s5d 0.489826 64 | 3s4s 0.465243 65 | 7hQh 0.428411 66 | ThAc 0.421081 67 | ThAd 0.421081 68 | 7sAs 0.415702 69 | 7h9h 0.408588 70 | 4h6h 0.391207 71 | 3h4h 0.387373 72 | 9sTs 0.367639 73 | 6h9h 0.359178 74 | 9cQc 0.336767 75 | 9dQd 0.336767 76 | TdJc 0.336202 77 | TcJd 0.336202 78 | 7hTh 0.327239 79 | TdJh 0.323304 80 | TcJh 0.323304 81 | 7hAc 0.312333 82 | 7hAd 0.312333 83 | ThAs 0.28378 84 | 4h7h 0.2764 85 | 6d6c 0.274163 86 | TdQd 0.259542 87 | TcQc 0.259542 88 | 7s8s 0.259143 89 | 5sAc 0.258624 90 | 5sAd 0.258624 91 | TdAd 0.255515 92 | TcAc 0.255515 93 | QhKh 0.255014 94 | 4hTh 0.247383 95 | ThJd 0.242815 96 | ThJc 0.242815 97 | 5dAs 0.24056 98 | 5cAs 0.24056 99 | 8c9h 0.240101 100 | 8d9h 0.240101 101 | 4s6s 0.240024 102 | TsJh 0.239936 103 | 8h9d 0.237024 104 | 8h9c 0.237024 105 | 8sTs 0.236164 106 | 3hJh 0.235815 107 | 5dAc 0.23249 108 | 5cAd 0.23249 109 | 6h6d 0.230447 110 | 6h6c 0.230447 111 | 6hTh 0.223814 112 | TcAd 0.222871 113 | TdAc 0.222871 114 | JhKh 0.217936 115 | 5s7s 0.217457 116 | 5s8s 0.209134 117 | 5cQc 0.205279 118 | 5dQd 0.205279 119 | 4hJh 0.204402 120 | 4s7s 0.190774 121 | 8hAd 0.188829 122 | 8hAc 0.188829 123 | TsQh 0.188518 124 | 6d7d 0.182818 125 | 6c7c 0.182818 126 | JsKs 0.182139 127 | 7hJh 0.180032 128 | ThJs 0.175976 129 | JcQc 0.174283 130 | JdQd 0.174283 131 | 6hQh 0.171648 132 | 9sJs 0.171103 133 | 6h7d 0.169491 134 | 6h7c 0.169491 135 | 9cTh 0.16643 136 | 9dTh 0.16643 137 | 6hJh 0.16181 138 | 6d7h 0.154108 139 | 6c7h 0.154108 140 | ThKh 0.15041 141 | TdQh 0.147366 142 | TcQh 0.147366 143 | 3hTh 0.145828 144 | 9cJh 0.143185 145 | 9dJh 0.143185 146 | 8dJh 0.135506 147 | 8cJh 0.135506 148 | 7h8c 0.135312 149 | 7h8d 0.135312 150 | JcKh 0.130459 151 | JdKh 0.130459 152 | 5c8c 0.130091 153 | 5d8d 0.130091 154 | 3s5s 0.129354 155 | 8hJh 0.127313 156 | 8sJs 0.125964 157 | 5d7d 0.12219 158 | 5c7c 0.12219 159 | 3h6h 0.122119 160 | 6hAd 0.12198 161 | 6hAc 0.12198 162 | 9cQh 0.120908 163 | 9dQh 0.120908 164 | 7c8h 0.119344 165 | 7d8h 0.119344 166 | 6cKh 0.11848 167 | 6dKh 0.11848 168 | 2h2c 0.118067 169 | 2h2d 0.118067 170 | 7cKh 0.113613 171 | 7dKh 0.113613 172 | 7h7c 0.111914 173 | 7h7d 0.111914 174 | TsKs 0.11125 175 | 4d5d 0.110589 176 | 4c5c 0.110589 177 | 9hQd 0.104607 178 | 9hQc 0.104607 179 | 2s2h 0.100401 180 | 9hTd 0.099982 181 | 9hTc 0.099982 182 | JsKh 0.098504 183 | 5s6s 0.098339 184 | 3s3h 0.097965 185 | 8dTh 0.095905 186 | 8cTh 0.095905 187 | 6s8s 0.095264 188 | TdKh 0.095156 189 | TcKh 0.095156 190 | 5s9s 0.089266 191 | 5d6d 0.087465 192 | 5c6c 0.087465 193 | TdQc 0.087005 194 | TcQd 0.087005 195 | 7sKs 0.086537 196 | 6hKh 0.084513 197 | 9hJh 0.084366 198 | TdJs 0.083068 199 | TcJs 0.083068 200 | 9dKh 0.081128 201 | 9cKh 0.081128 202 | 3h3d 0.079948 203 | 3h3c 0.079948 204 | 8h8c 0.077443 205 | 8h8d 0.077443 206 | 9sQh 0.077198 207 | JsQh 0.076678 208 | 4h4d 0.073616 209 | 4h4c 0.073616 210 | 4h8h 0.073565 211 | 9dQc 0.071635 212 | 9cQd 0.071635 213 | 3d5d 0.071211 214 | 3c5c 0.071211 215 | 8cKh 0.071153 216 | 8dKh 0.071153 217 | 7hKh 0.066619 218 | 4hKh 0.066001 219 | 4s5s 0.063675 220 | 5sQc 0.063187 221 | 5sQd 0.063187 222 | 5c9c 0.062731 223 | 5d9d 0.062731 224 | JcQd 0.061811 225 | JdQc 0.061811 226 | TsKh 0.061088 227 | 8hKh 0.056488 228 | JhKc 0.056085 229 | 9hJd 0.05348 230 | 9hJc 0.05348 231 | 3hKh 0.050775 232 | 8hQd 0.05038 233 | 8hQc 0.05038 234 | 3s6s 0.049144 235 | TsQc 0.049088 236 | TsQd 0.049088 237 | 6sAs 0.046578 238 | ThQc 0.045762 239 | ThQd 0.045762 240 | JcQh 0.045513 241 | JdQh 0.045513 242 | 8hJd 0.045208 243 | 8hJc 0.045208 244 | 5sKs 0.044395 245 | 9sKs 0.044308 246 | JcKc 0.043908 247 | 5cQd 0.039281 248 | 5dQc 0.039281 249 | 8dQd 0.035404 250 | 8cQc 0.035404 251 | 6d7c 0.035172 252 | 6c7d 0.035172 253 | 7sTs 0.035108 254 | TsJd 0.032898 255 | TsJc 0.032898 256 | 7hAs 0.032515 257 | 9hKc 0.03239 258 | 6hAs 0.030463 259 | 8sKs 0.030326 260 | 6s6d 0.028536 261 | 6s6c 0.028536 262 | 7hKc 0.026387 263 | 8s9h 0.025309 264 | 9h9c 0.025287 265 | 9h9d 0.025287 266 | 8hTc 0.024807 267 | 8hTd 0.024807 268 | 3hQh 0.024048 269 | 9s9h 0.022733 270 | 2hJh 0.022645 271 | 7hQc 0.021782 272 | 7hQd 0.021782 273 | JdKc 0.020455 274 | 4d5c 0.019111 275 | 4c5d 0.019111 276 | 6h8d 0.017399 277 | 6h8c 0.017399 278 | 6sKs 0.017271 279 | 5dQh 0.016629 280 | 5cQh 0.016629 281 | 3sTs 0.016566 282 | 9hKs 0.016399 283 | TcKc 0.014618 284 | 5sQh 0.014519 285 | 7d7c 0.014207 286 | 6hKc 0.012789 287 | 5cKc 0.012455 288 | 8h9s 0.012401 289 | 8hKc 0.010877 290 | TsAc 0.009255 291 | TsAd 0.009255 292 | 2hTh 0.008329 293 | 6d8h 0.008122 294 | 6c8h 0.008122 295 | JsQd 0.00778 296 | JsQc 0.00778 297 | 3h9h 0.007491 298 | 4s4h 0.007365 299 | TdKc 0.007048 300 | 4sAs 0.006925 301 | 9sQc 0.006557 302 | 9sQd 0.006557 303 | 5dKh 0.00609 304 | 5cKh 0.00609 305 | 5dKc 0.006084 306 | 2hQh 0.005711 307 | 4c5s 0.005594 308 | 4d5s 0.005594 309 | 4hQh 0.005377 310 | QcKc 0.005261 311 | 9hAc 0.005189 312 | 9hAd 0.005189 313 | JhQd 0.004636 314 | JhQc 0.004636 315 | 5c6d 0.004245 316 | 5d6c 0.004245 317 | 4h5d 0.004108 318 | 4h5c 0.004108 319 | 9dAd 0.003654 320 | 9cAc 0.003654 321 | 5sKh 0.002873 322 | ThTd 0.002718 323 | ThTc 0.002718 324 | 7s7c 0.002255 325 | 7s7d 0.002255 326 | QdKc 0.002196 327 | 4h5s 0.001528 328 | 5s6d 0.001469 329 | 5s6c 0.001469 330 | 7sQh 0.001327 331 | 8dQh 0.000626 332 | 8cQh 0.000626 333 | 4h9h 0.00042 334 | 2sTs 0.000129 335 | 9dAc 0.000111 336 | 9cAd 0.000111 337 | -------------------------------------------------------------------------------- /src/nn/modules/batch_norm.py: -------------------------------------------------------------------------------- 1 | 2 | import torch 3 | 4 | import settings.arguments as arguments 5 | 6 | from nn.modules.module import Module 7 | from nn.modules.utils import clear 8 | 9 | import utils.pseudo_random as pseudo_random 10 | 11 | 12 | class BatchNormalization(Module): 13 | # expected dimension of input 14 | nDim = 2 15 | 16 | def __init__(self, nOutput, eps=1e-5, momentum=0.1, affine=True): 17 | super(BatchNormalization, self).__init__() 18 | assert nOutput != 0 19 | 20 | self.affine = affine 21 | self.eps = eps 22 | self.train = True 23 | self.momentum = momentum 24 | self.running_mean = torch.zeros(nOutput).type(arguments.Tensor) 25 | self.running_var = torch.ones(nOutput).type(arguments.Tensor) 26 | 27 | self.save_mean: torch.Tensor = None 28 | self.save_std: torch.Tensor = None 29 | self.save_center: torch.Tensor = None 30 | self.save_norm: torch.Tensor = None 31 | self._input = None 32 | self._gradOutput = None 33 | 34 | if self.affine: 35 | self.weight = arguments.Tensor(nOutput) 36 | self.bias = arguments.Tensor(nOutput) 37 | self.gradWeight = arguments.Tensor(nOutput) 38 | self.gradBias = arguments.Tensor(nOutput) 39 | self.reset() 40 | else: 41 | self.weight = None 42 | self.bias = None 43 | self.gradWeight = None 44 | self.gradBias = None 45 | 46 | def update_output(self, input) -> torch.Tensor: 47 | self._check_input_dim(input) 48 | 49 | input = self._make_contiguous(input)[0] 50 | 51 | self.output = input.new(input.size(0), input.size(1)) 52 | 53 | if self.save_mean is None: 54 | self.save_mean = input.new() 55 | self.save_mean.resize_as_(self.running_mean) 56 | if self.save_std is None: 57 | self.save_std = input.new() 58 | self.save_std.resize_as_(self.running_var) 59 | 60 | self.output = self._forward(input, self.weight, self.bias, self.train, self.momentum, self.eps) 61 | return self.output 62 | 63 | def update_grad_input(self, input, gradOutput): 64 | return self._backward(input, gradOutput, 1., self.gradInput) 65 | 66 | def backward(self, input, gradOutput, scale=1.): 67 | return self._backward(input, gradOutput, scale, self.gradInput, self.gradWeight, self.gradBias) 68 | 69 | def acc_grad_parameters(self, input, gradOutput, scale=1.): 70 | return self._backward(input, gradOutput, scale, None, self.gradWeight, self.gradBias) 71 | 72 | def _forward(self, input: torch.Tensor, weight: torch.Tensor, bias, train, momentum, eps): 73 | if train: 74 | sample_mean = torch.mean(input, 0) 75 | sample_var = torch.var(input, 0, unbiased=False) 76 | 77 | self.running_mean = momentum * sample_mean + (1 - momentum) * self.running_mean 78 | self.running_var = momentum * sample_var + (1 - momentum) * self.running_var 79 | 80 | self.save_std = torch.sqrt(sample_var + eps) 81 | self.save_mean = sample_mean 82 | self.save_center = input - sample_mean 83 | self.save_norm = self.save_center / self.save_std 84 | 85 | self.output = weight * self.save_norm + bias 86 | else: 87 | self.output = (input - self.running_mean) / torch.sqrt(self.running_var + eps) 88 | self.output = weight * self.output + bias 89 | return self.output 90 | 91 | def _backward(self, input, gradOutput, scale, gradInput=None, gradWeight=None, gradBias=None): 92 | self._check_input_dim(input) 93 | self._check_input_dim(gradOutput) 94 | if not hasattr(self, 'save_mean') or not hasattr(self, 'save_std'): 95 | raise RuntimeError('you have to call updateOutput() at least once before backward()') 96 | 97 | input, gradOutput = self._make_contiguous(input, gradOutput) 98 | N = gradOutput.size(0) 99 | 100 | scale = scale or 1. 101 | if gradInput is not None: 102 | gradInput.resize_as_(gradOutput).zero_() 103 | 104 | gradWeight.copy_(torch.sum(gradOutput * self.save_norm, 0)) 105 | gradBias.copy_(torch.sum(gradOutput, 0)) 106 | 107 | dx_norm = gradOutput * self.weight 108 | self.gradInput = 1 / N / self.save_std * ( 109 | N * dx_norm - torch.sum(dx_norm, 0) - self.save_norm * torch.sum(dx_norm * self.save_norm, 0)) 110 | return self.gradInput 111 | 112 | def reset(self): 113 | arguments.logger.trace(f"Resetting 'BatchNormalization' module with size {repr(self.weight.size())}") 114 | 115 | if arguments.use_pseudo_random: 116 | if self.weight is not None: 117 | pseudo_random.uniform_(self.weight, 0.0, 1.0) 118 | else: 119 | if self.weight is not None: 120 | self.weight.uniform_() 121 | 122 | if self.bias is not None: 123 | self.bias.zero_() 124 | 125 | self.running_mean.zero_() 126 | self.running_var.fill_(1) 127 | 128 | def clear_state(self): 129 | # first 5 buffers are not present in the current implementation, 130 | # but we keep them for cleaning old saved models 131 | clear(self, [ 132 | 'buffer', 133 | 'buffer2', 134 | 'centered', 135 | 'std', 136 | 'normalized', 137 | '_input', 138 | '_gradOutput', 139 | 'save_mean', 140 | 'save_std', 141 | ]) 142 | return super(BatchNormalization, self).clear_state() 143 | 144 | def _check_input_dim(self, input): 145 | if input.dim() != self.nDim: 146 | raise RuntimeError( 147 | 'only mini-batch supported ({}D tensor), got {}D tensor instead'.format(self.nDim, input.dim())) 148 | if input.size(1) != self.running_mean.nelement(): 149 | raise RuntimeError('got {}-feature tensor, expected {}'.format(input.size(1), self.running_mean.nelement())) 150 | 151 | def _make_contiguous(self, input, gradOutput=None): 152 | if not input.is_contiguous(): 153 | if self._input is None: 154 | self._input = input.new() 155 | self._input.resize_as_(input).copy_(input) 156 | input = self._input 157 | 158 | if gradOutput is not None: 159 | if not gradOutput.is_contiguous(): 160 | if self._gradOutput is None: 161 | self._gradOutput = gradOutput.new() 162 | self._gradOutput.resize_as_(gradOutput).copy_(gradOutput) 163 | gradOutput = self._gradOutput 164 | 165 | return input, gradOutput 166 | -------------------------------------------------------------------------------- /src/torch7/torch7_file.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from collections import OrderedDict 3 | 4 | from nn.modules import module_factory 5 | from torch7 import torch7_factory 6 | 7 | TYPE_NIL = 0 8 | TYPE_NUMBER = 1 9 | TYPE_STRING = 2 10 | TYPE_TABLE = 3 11 | TYPE_TORCH = 4 12 | TYPE_BOOLEAN = 5 13 | TYPE_FUNCTION = 6 14 | TYPE_RECUR_FUNCTION = 8 15 | LEGACY_TYPE_RECUR_FUNCTION = 7 16 | 17 | module_factory = module_factory.ModuleFactory() 18 | torch7_factory = torch7_factory.TorchFactory() 19 | 20 | 21 | class Torch7File(object): 22 | 23 | def __init__(self, file_obj): 24 | self.file = file_obj 25 | self.file_objects = {} 26 | self.index = 0 27 | 28 | def read_torch7_object(self): 29 | if 'b' in self.file.mode: 30 | return self.read_torch7_object_binary() 31 | else: 32 | return self.read_torch7_object_ascii() 33 | 34 | def read_torch7_object_ascii(self): 35 | type_line = self.file.readline().strip() 36 | if type_line: 37 | typeidx = int(type_line) 38 | 39 | if typeidx == TYPE_NUMBER: 40 | return float(self.file.readline().strip()) 41 | elif typeidx == TYPE_BOOLEAN: 42 | return bool(self.file.readline().strip()) 43 | elif typeidx == TYPE_STRING: 44 | size = int(self.file.readline().strip()) 45 | return str(self.file.read(size + 1)).strip() 46 | elif typeidx == TYPE_FUNCTION: 47 | raise NotImplementedError() 48 | elif typeidx in {TYPE_TABLE, TYPE_TORCH, TYPE_RECUR_FUNCTION, LEGACY_TYPE_RECUR_FUNCTION}: 49 | self.index = int(self.file.readline()) 50 | if self.index in self.file_objects: 51 | return self.file_objects[self.index] 52 | 53 | if typeidx in {TYPE_RECUR_FUNCTION, LEGACY_TYPE_RECUR_FUNCTION}: 54 | raise NotImplementedError() 55 | elif typeidx == TYPE_TORCH: 56 | version = str(self.file.read(int(self.file.readline())+1)).strip() # only accept version V 1 or later 57 | class_name = str(self.file.read(int(self.file.readline())+1)).strip() 58 | if class_name.startswith("nn"): 59 | torch_object = module_factory.create_module(class_name) 60 | elif class_name == "torch.FloatTensor" or class_name == "torch.CudaTensor": 61 | torch_object = torch7_factory.create_torch_object(class_name) 62 | else: 63 | raise TypeError() 64 | self.file_objects[self.index] = torch_object 65 | 66 | if torch_object is not None: 67 | if hasattr(torch_object, "read"): 68 | torch_object.__getattribute__("read")(self) 69 | else: 70 | raise NameError() 71 | 72 | return torch_object 73 | else: # it's a torch table object 74 | size = int(self.file.readline().strip()) 75 | torch_object = OrderedDict() 76 | self.file_objects[self.index] = torch_object 77 | 78 | for i in range(size): 79 | k = self.read_torch7_object() 80 | v = self.read_torch7_object() 81 | torch_object[k] = v 82 | 83 | return torch_object 84 | else: 85 | raise TypeError() 86 | else: 87 | # reached EOF 88 | return 89 | 90 | def read_torch7_object_binary(self): 91 | type_line = self.file.read(4) 92 | if type_line: 93 | typeidx = struct.unpack('i', type_line)[0] 94 | 95 | if typeidx == TYPE_NUMBER: 96 | return float(struct.unpack('d', self.file.read(8))[0]) 97 | elif typeidx == TYPE_BOOLEAN: 98 | return bool(struct.unpack('i', self.file.read(4))[0]) 99 | elif typeidx == TYPE_STRING: 100 | size = int(struct.unpack('i', self.file.read(4))[0]) 101 | return str(self.file.read(size), 'utf-8') 102 | elif typeidx == TYPE_FUNCTION: 103 | raise NotImplementedError() 104 | elif typeidx in {TYPE_TABLE, TYPE_TORCH, TYPE_RECUR_FUNCTION, LEGACY_TYPE_RECUR_FUNCTION}: 105 | self.index = int(struct.unpack('i', self.file.read(4))[0]) 106 | if self.index in self.file_objects: 107 | return self.file_objects[self.index] 108 | 109 | if typeidx in {TYPE_RECUR_FUNCTION, LEGACY_TYPE_RECUR_FUNCTION}: 110 | raise NotImplementedError() 111 | elif typeidx == TYPE_TORCH: 112 | version = str(self.file.read(int(struct.unpack('i', self.file.read(4))[0])), 'utf-8') # only accept version V 1 or later 113 | class_name = str(self.file.read(int(struct.unpack('i', self.file.read(4))[0])), 'utf-8') 114 | if class_name.startswith("nn"): 115 | torch_object = module_factory.create_module(class_name) 116 | elif class_name == "torch.FloatTensor" or class_name == "torch.CudaTensor": 117 | torch_object = torch7_factory.create_torch_object(class_name) 118 | else: 119 | raise TypeError() 120 | self.file_objects[self.index] = torch_object 121 | 122 | if torch_object is not None: 123 | if hasattr(torch_object, "read"): 124 | torch_object.__getattribute__("read")(self) 125 | else: 126 | raise NameError() 127 | 128 | return torch_object 129 | else: # it's a torch table object 130 | size = int(struct.unpack('i', self.file.read(4))[0]) 131 | torch_object = OrderedDict() 132 | self.file_objects[self.index] = torch_object 133 | 134 | for i in range(size): 135 | k = self.read_torch7_object() 136 | v = self.read_torch7_object() 137 | torch_object[k] = v 138 | 139 | return torch_object 140 | else: 141 | raise TypeError() 142 | else: 143 | # reached EOF 144 | return 145 | 146 | 147 | def read_model_from_torch7_file(file_name, mode="r"): 148 | with open(file_name, mode) as file_obj: 149 | src_file = Torch7File(file_obj) 150 | return src_file.read_torch7_object() 151 | 152 | 153 | --------------------------------------------------------------------------------