Deep CFR playability encoding 추가
This commit is contained in:
@@ -4,7 +4,12 @@
|
|||||||
from coolrl_lost_cities.games.classic.game cimport GameState
|
from coolrl_lost_cities.games.classic.game cimport GameState
|
||||||
|
|
||||||
|
|
||||||
cdef int input_dim_c(GameState state) noexcept:
|
cdef int DERIVED_PLAYABILITY_PER_COLOR = 19
|
||||||
|
cdef int DERIVED_PLAYABILITY_COMMON = 3
|
||||||
|
cdef int SLOT_AWARE_PLAYABILITY_PER_SLOT = 12
|
||||||
|
|
||||||
|
|
||||||
|
cdef int _base_input_dim_c(GameState state) noexcept:
|
||||||
cdef int action_size = 2 * state.hand_size + 1 + state.n_colors
|
cdef int action_size = 2 * state.hand_size + 1 + state.n_colors
|
||||||
cdef int card_type_size = state.n_colors * (state.n_ranks + 1)
|
cdef int card_type_size = state.n_colors * (state.n_ranks + 1)
|
||||||
return (
|
return (
|
||||||
@@ -20,7 +25,274 @@ cdef int input_dim_c(GameState state) noexcept:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
cdef int input_dim_c(GameState state) noexcept:
|
||||||
|
return _input_dim_with_flags_c(state, False, False)
|
||||||
|
|
||||||
|
|
||||||
|
cdef int _input_dim_with_flags_c(GameState state, bint derived_playability, bint slot_aware_playability) noexcept:
|
||||||
|
cdef int total = _base_input_dim_c(state)
|
||||||
|
if derived_playability:
|
||||||
|
total += state.n_colors * DERIVED_PLAYABILITY_PER_COLOR + DERIVED_PLAYABILITY_COMMON
|
||||||
|
if slot_aware_playability:
|
||||||
|
total += state.hand_size * SLOT_AWARE_PLAYABILITY_PER_SLOT
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
cdef int _numeric_value_c(GameState state, int rank) noexcept:
|
||||||
|
if rank == 0:
|
||||||
|
return 0
|
||||||
|
return state.min_rank + rank - 1
|
||||||
|
|
||||||
|
|
||||||
|
cdef float _max_numeric_sum_c(GameState state) noexcept:
|
||||||
|
return <float>(state.n_ranks * (2 * state.min_rank + state.n_ranks - 1)) / 2.0
|
||||||
|
|
||||||
|
|
||||||
|
cdef float _max_score_estimate_c(GameState state) noexcept:
|
||||||
|
cdef float max_numeric_sum = _max_numeric_sum_c(state)
|
||||||
|
cdef float break_even = <float>abs(state.expedition_penalty)
|
||||||
|
cdef float estimate = (max_numeric_sum - break_even) * <float>(state.n_handshakes + 1)
|
||||||
|
if estimate < 1.0:
|
||||||
|
return 1.0
|
||||||
|
return estimate
|
||||||
|
|
||||||
|
|
||||||
|
cdef void _color_playability_summary_c(
|
||||||
|
GameState state,
|
||||||
|
int player,
|
||||||
|
int color,
|
||||||
|
int* is_unopened,
|
||||||
|
int* has_only_wagers_opened,
|
||||||
|
int* current_numeric_sum,
|
||||||
|
int* current_wager_count,
|
||||||
|
int* current_expedition_len,
|
||||||
|
int* last_numeric_rank,
|
||||||
|
int* hand_count,
|
||||||
|
int* hand_wager_count,
|
||||||
|
int* playable_hand_wager_count,
|
||||||
|
int* playable_hand_numeric_sum,
|
||||||
|
int* playable_hand_numeric_count,
|
||||||
|
int* dead_hand_numeric_count,
|
||||||
|
int* dead_hand_numeric_sum,
|
||||||
|
int* recoverable_margin_no_bonus,
|
||||||
|
int* recoverable_score_no_bonus,
|
||||||
|
int* min_needed_to_break_even,
|
||||||
|
int* discard_top_playable_flag,
|
||||||
|
int* discard_top_playable_value,
|
||||||
|
int* unknown_remaining_count,
|
||||||
|
int* has_bonus_path,
|
||||||
|
int* cards_needed_for_bonus,
|
||||||
|
) noexcept:
|
||||||
|
cdef int cache_index = state._expedition_len_index(player, color)
|
||||||
|
cdef int opponent_index = state._expedition_len_index(1 - player, color)
|
||||||
|
cdef int slot
|
||||||
|
cdef int card
|
||||||
|
cdef int rank
|
||||||
|
cdef int length = state.expedition_lens[cache_index]
|
||||||
|
cdef int projected_numeric_sum
|
||||||
|
cdef int projected_wager_count
|
||||||
|
cdef int projected_len
|
||||||
|
cdef int break_even = abs(state.expedition_penalty)
|
||||||
|
cdef int known_color_count
|
||||||
|
cdef int top_card
|
||||||
|
|
||||||
|
current_numeric_sum[0] = state.numeric_sums[cache_index]
|
||||||
|
current_wager_count[0] = state.handshake_counts[cache_index]
|
||||||
|
current_expedition_len[0] = length
|
||||||
|
last_numeric_rank[0] = state.last_numeric_ranks[cache_index]
|
||||||
|
is_unopened[0] = 1 if length == 0 else 0
|
||||||
|
has_only_wagers_opened[0] = 1 if length > 0 and last_numeric_rank[0] == 0 else 0
|
||||||
|
|
||||||
|
hand_count[0] = 0
|
||||||
|
hand_wager_count[0] = 0
|
||||||
|
playable_hand_wager_count[0] = 0
|
||||||
|
playable_hand_numeric_sum[0] = 0
|
||||||
|
playable_hand_numeric_count[0] = 0
|
||||||
|
dead_hand_numeric_count[0] = 0
|
||||||
|
dead_hand_numeric_sum[0] = 0
|
||||||
|
|
||||||
|
for slot in range(state.hand_lens[player]):
|
||||||
|
card = state.hand_cards[state._hand_index(player, slot)]
|
||||||
|
if state._card_color(card) != color:
|
||||||
|
continue
|
||||||
|
rank = state._card_rank(card)
|
||||||
|
hand_count[0] += 1
|
||||||
|
if rank == 0:
|
||||||
|
hand_wager_count[0] += 1
|
||||||
|
if last_numeric_rank[0] == 0:
|
||||||
|
playable_hand_wager_count[0] += 1
|
||||||
|
elif rank > last_numeric_rank[0]:
|
||||||
|
playable_hand_numeric_count[0] += 1
|
||||||
|
playable_hand_numeric_sum[0] += _numeric_value_c(state, rank)
|
||||||
|
else:
|
||||||
|
dead_hand_numeric_count[0] += 1
|
||||||
|
dead_hand_numeric_sum[0] += _numeric_value_c(state, rank)
|
||||||
|
|
||||||
|
projected_numeric_sum = current_numeric_sum[0] + playable_hand_numeric_sum[0]
|
||||||
|
projected_wager_count = current_wager_count[0] + playable_hand_wager_count[0]
|
||||||
|
recoverable_margin_no_bonus[0] = projected_numeric_sum - break_even
|
||||||
|
recoverable_score_no_bonus[0] = recoverable_margin_no_bonus[0] * (projected_wager_count + 1)
|
||||||
|
min_needed_to_break_even[0] = max(0, break_even - projected_numeric_sum)
|
||||||
|
projected_len = length + playable_hand_numeric_count[0] + playable_hand_wager_count[0]
|
||||||
|
has_bonus_path[0] = 1 if projected_len >= state.bonus_threshold else 0
|
||||||
|
cards_needed_for_bonus[0] = max(0, state.bonus_threshold - projected_len)
|
||||||
|
|
||||||
|
discard_top_playable_flag[0] = 0
|
||||||
|
discard_top_playable_value[0] = 0
|
||||||
|
if state.discard_lens[color] > 0:
|
||||||
|
top_card = state.discard_cards[state._discard_index(color, state.discard_lens[color] - 1)]
|
||||||
|
rank = state._card_rank(top_card)
|
||||||
|
if state._card_color(top_card) == color and rank > 0 and rank > last_numeric_rank[0]:
|
||||||
|
discard_top_playable_flag[0] = 1
|
||||||
|
discard_top_playable_value[0] = _numeric_value_c(state, rank)
|
||||||
|
|
||||||
|
known_color_count = hand_count[0]
|
||||||
|
known_color_count += state.expedition_lens[cache_index]
|
||||||
|
known_color_count += state.expedition_lens[opponent_index]
|
||||||
|
known_color_count += state.discard_lens[color]
|
||||||
|
unknown_remaining_count[0] = max(0, state.cards_per_color - known_color_count)
|
||||||
|
|
||||||
|
|
||||||
|
cdef int _append_derived_playability_features_c(GameState state, int player, float* out, int idx) noexcept:
|
||||||
|
cdef float max_numeric_sum = _max_numeric_sum_c(state)
|
||||||
|
cdef float max_cards_per_color = <float>max(1, state.cards_per_color)
|
||||||
|
cdef float max_wagers = <float>max(1, state.n_handshakes)
|
||||||
|
cdef float max_score_estimate = _max_score_estimate_c(state)
|
||||||
|
cdef int color
|
||||||
|
cdef int is_unopened, has_only_wagers_opened, current_numeric_sum, current_wager_count
|
||||||
|
cdef int current_expedition_len, last_numeric_rank, hand_count, hand_wager_count
|
||||||
|
cdef int playable_hand_wager_count, playable_hand_numeric_sum, playable_hand_numeric_count
|
||||||
|
cdef int dead_hand_numeric_count, dead_hand_numeric_sum, recoverable_margin_no_bonus
|
||||||
|
cdef int recoverable_score_no_bonus, min_needed_to_break_even, discard_top_playable_flag
|
||||||
|
cdef int discard_top_playable_value, unknown_remaining_count, has_bonus_path
|
||||||
|
cdef int cards_needed_for_bonus
|
||||||
|
|
||||||
|
for color in range(state.n_colors):
|
||||||
|
_color_playability_summary_c(
|
||||||
|
state, player, color,
|
||||||
|
&is_unopened, &has_only_wagers_opened, ¤t_numeric_sum,
|
||||||
|
¤t_wager_count, ¤t_expedition_len, &last_numeric_rank,
|
||||||
|
&hand_count, &hand_wager_count, &playable_hand_wager_count,
|
||||||
|
&playable_hand_numeric_sum, &playable_hand_numeric_count,
|
||||||
|
&dead_hand_numeric_count, &dead_hand_numeric_sum, &recoverable_margin_no_bonus,
|
||||||
|
&recoverable_score_no_bonus, &min_needed_to_break_even,
|
||||||
|
&discard_top_playable_flag, &discard_top_playable_value,
|
||||||
|
&unknown_remaining_count, &has_bonus_path, &cards_needed_for_bonus,
|
||||||
|
)
|
||||||
|
out[idx] = <float>is_unopened
|
||||||
|
out[idx + 1] = <float>has_only_wagers_opened
|
||||||
|
out[idx + 2] = <float>current_numeric_sum / max_numeric_sum
|
||||||
|
out[idx + 3] = <float>current_wager_count / max_wagers
|
||||||
|
out[idx + 4] = <float>current_expedition_len / max_cards_per_color
|
||||||
|
out[idx + 5] = <float>last_numeric_rank / max_numeric_sum
|
||||||
|
out[idx + 6] = <float>hand_count / max_cards_per_color
|
||||||
|
out[idx + 7] = <float>hand_wager_count / max_wagers
|
||||||
|
out[idx + 8] = <float>playable_hand_numeric_sum / max_numeric_sum
|
||||||
|
out[idx + 9] = <float>playable_hand_numeric_count / max_cards_per_color
|
||||||
|
out[idx + 10] = <float>dead_hand_numeric_count / max_cards_per_color
|
||||||
|
out[idx + 11] = <float>dead_hand_numeric_sum / max_numeric_sum
|
||||||
|
out[idx + 12] = <float>recoverable_margin_no_bonus / max_numeric_sum
|
||||||
|
out[idx + 13] = <float>recoverable_score_no_bonus / max_score_estimate
|
||||||
|
out[idx + 14] = <float>min_needed_to_break_even / max_numeric_sum
|
||||||
|
out[idx + 15] = <float>discard_top_playable_flag
|
||||||
|
out[idx + 16] = <float>discard_top_playable_value / max_numeric_sum
|
||||||
|
out[idx + 17] = <float>unknown_remaining_count / max_cards_per_color
|
||||||
|
out[idx + 18] = <float>cards_needed_for_bonus / max_cards_per_color
|
||||||
|
idx += DERIVED_PLAYABILITY_PER_COLOR
|
||||||
|
|
||||||
|
out[idx] = <float>state.deck_len / <float>max(1, state.total_cards)
|
||||||
|
out[idx + 1] = <float>state.turn_count / <float>max(1, 2 * state.total_cards)
|
||||||
|
out[idx + 2] = <float>state.deck_len / <float>max(1, 2 * state.total_cards)
|
||||||
|
return idx + DERIVED_PLAYABILITY_COMMON
|
||||||
|
|
||||||
|
|
||||||
|
cdef int _append_slot_aware_playability_features_c(GameState state, int player, float* out, int idx) noexcept:
|
||||||
|
cdef float max_numeric_sum = _max_numeric_sum_c(state)
|
||||||
|
cdef float max_score_estimate = _max_score_estimate_c(state)
|
||||||
|
cdef int slot
|
||||||
|
cdef int card
|
||||||
|
cdef int color
|
||||||
|
cdef int rank
|
||||||
|
cdef int is_unopened, has_only_wagers_opened, current_numeric_sum, current_wager_count
|
||||||
|
cdef int current_expedition_len, last_numeric_rank, hand_count, hand_wager_count
|
||||||
|
cdef int playable_hand_wager_count, playable_hand_numeric_sum, playable_hand_numeric_count
|
||||||
|
cdef int dead_hand_numeric_count, dead_hand_numeric_sum, recoverable_margin_no_bonus
|
||||||
|
cdef int recoverable_score_no_bonus, min_needed_to_break_even, discard_top_playable_flag
|
||||||
|
cdef int discard_top_playable_value, unknown_remaining_count, has_bonus_path
|
||||||
|
cdef int cards_needed_for_bonus
|
||||||
|
cdef bint legal_play
|
||||||
|
cdef bint has_numeric_started
|
||||||
|
cdef bint is_numeric
|
||||||
|
cdef bint is_wager
|
||||||
|
cdef bint would_start_color_commitment
|
||||||
|
cdef bint is_playable_to_existing
|
||||||
|
cdef bint is_dead_numeric
|
||||||
|
cdef bint is_wager_before_numeric
|
||||||
|
cdef bint is_numeric_open
|
||||||
|
cdef bint is_wager_first_open
|
||||||
|
cdef bint is_bad_open_candidate
|
||||||
|
cdef bint is_safe_continuation
|
||||||
|
cdef float open_risk_score
|
||||||
|
|
||||||
|
for slot in range(state.hand_size):
|
||||||
|
if slot >= state.hand_lens[player]:
|
||||||
|
idx += SLOT_AWARE_PLAYABILITY_PER_SLOT
|
||||||
|
continue
|
||||||
|
card = state.hand_cards[state._hand_index(player, slot)]
|
||||||
|
color = state._card_color(card)
|
||||||
|
rank = state._card_rank(card)
|
||||||
|
_color_playability_summary_c(
|
||||||
|
state, player, color,
|
||||||
|
&is_unopened, &has_only_wagers_opened, ¤t_numeric_sum,
|
||||||
|
¤t_wager_count, ¤t_expedition_len, &last_numeric_rank,
|
||||||
|
&hand_count, &hand_wager_count, &playable_hand_wager_count,
|
||||||
|
&playable_hand_numeric_sum, &playable_hand_numeric_count,
|
||||||
|
&dead_hand_numeric_count, &dead_hand_numeric_sum, &recoverable_margin_no_bonus,
|
||||||
|
&recoverable_score_no_bonus, &min_needed_to_break_even,
|
||||||
|
&discard_top_playable_flag, &discard_top_playable_value,
|
||||||
|
&unknown_remaining_count, &has_bonus_path, &cards_needed_for_bonus,
|
||||||
|
)
|
||||||
|
legal_play = state._can_play_encoded_card_c(player, card)
|
||||||
|
has_numeric_started = last_numeric_rank > 0
|
||||||
|
is_numeric = rank > 0
|
||||||
|
is_wager = rank == 0
|
||||||
|
would_start_color_commitment = legal_play and not has_numeric_started
|
||||||
|
is_numeric_open = would_start_color_commitment and is_numeric
|
||||||
|
is_wager_first_open = would_start_color_commitment and is_wager and is_unopened
|
||||||
|
is_playable_to_existing = legal_play and has_numeric_started
|
||||||
|
is_dead_numeric = is_numeric and not legal_play and rank <= last_numeric_rank
|
||||||
|
is_wager_before_numeric = is_wager and legal_play and not has_numeric_started
|
||||||
|
is_bad_open_candidate = would_start_color_commitment and recoverable_score_no_bonus < 0
|
||||||
|
open_risk_score = min(0.0, <float>recoverable_score_no_bonus) if would_start_color_commitment else 0.0
|
||||||
|
is_safe_continuation = (not would_start_color_commitment) and is_playable_to_existing
|
||||||
|
|
||||||
|
out[idx] = <float>recoverable_score_no_bonus / max_score_estimate
|
||||||
|
out[idx + 1] = <float>recoverable_margin_no_bonus / max_numeric_sum
|
||||||
|
out[idx + 2] = <float>would_start_color_commitment
|
||||||
|
out[idx + 3] = <float>is_numeric_open
|
||||||
|
out[idx + 4] = <float>is_wager_first_open
|
||||||
|
out[idx + 5] = <float>is_playable_to_existing
|
||||||
|
out[idx + 6] = <float>is_dead_numeric
|
||||||
|
out[idx + 7] = <float>is_wager_before_numeric
|
||||||
|
out[idx + 8] = <float>has_bonus_path
|
||||||
|
out[idx + 9] = <float>is_bad_open_candidate
|
||||||
|
out[idx + 10] = open_risk_score / max_score_estimate
|
||||||
|
out[idx + 11] = <float>is_safe_continuation
|
||||||
|
idx += SLOT_AWARE_PLAYABILITY_PER_SLOT
|
||||||
|
return idx
|
||||||
|
|
||||||
|
|
||||||
cdef int encode_info_state_c(GameState state, int player, float* out) except -1:
|
cdef int encode_info_state_c(GameState state, int player, float* out) except -1:
|
||||||
|
return _encode_info_state_with_flags_c(state, player, out, False, False)
|
||||||
|
|
||||||
|
|
||||||
|
cdef int _encode_info_state_with_flags_c(
|
||||||
|
GameState state,
|
||||||
|
int player,
|
||||||
|
float* out,
|
||||||
|
bint derived_playability,
|
||||||
|
bint slot_aware_playability,
|
||||||
|
) except -1:
|
||||||
cdef int idx = 0
|
cdef int idx = 0
|
||||||
cdef int slot
|
cdef int slot
|
||||||
cdef int card
|
cdef int card
|
||||||
@@ -132,18 +404,32 @@ cdef int encode_info_state_c(GameState state, int player, float* out) except -1:
|
|||||||
for i in range(action_count):
|
for i in range(action_count):
|
||||||
out[idx + actions[i]] = 1.0
|
out[idx + actions[i]] = 1.0
|
||||||
idx += action_size
|
idx += action_size
|
||||||
|
if derived_playability:
|
||||||
|
idx = _append_derived_playability_features_c(state, player, out, idx)
|
||||||
|
if slot_aware_playability:
|
||||||
|
idx = _append_slot_aware_playability_features_c(state, player, out, idx)
|
||||||
return idx
|
return idx
|
||||||
|
|
||||||
|
|
||||||
def input_dim(GameState state) -> int:
|
def input_dim(GameState state, encoding=None) -> int:
|
||||||
return input_dim_c(state)
|
cdef bint derived_playability = False
|
||||||
|
cdef bint slot_aware_playability = False
|
||||||
|
if encoding is not None:
|
||||||
|
derived_playability = bool(encoding.derived_playability)
|
||||||
|
slot_aware_playability = bool(encoding.slot_aware_playability)
|
||||||
|
return _input_dim_with_flags_c(state, derived_playability, slot_aware_playability)
|
||||||
|
|
||||||
|
|
||||||
def encode_info_state(GameState state, int player):
|
def encode_info_state(GameState state, int player, encoding=None):
|
||||||
cdef float[::1] out_view
|
cdef float[::1] out_view
|
||||||
|
cdef bint derived_playability = False
|
||||||
|
cdef bint slot_aware_playability = False
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
out = np.empty(input_dim_c(state), dtype=np.float32)
|
if encoding is not None:
|
||||||
|
derived_playability = bool(encoding.derived_playability)
|
||||||
|
slot_aware_playability = bool(encoding.slot_aware_playability)
|
||||||
|
out = np.empty(_input_dim_with_flags_c(state, derived_playability, slot_aware_playability), dtype=np.float32)
|
||||||
out_view = out
|
out_view = out
|
||||||
encode_info_state_c(state, player, &out_view[0])
|
_encode_info_state_with_flags_c(state, player, &out_view[0], derived_playability, slot_aware_playability)
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import numpy as np
|
|||||||
import torch
|
import torch
|
||||||
|
|
||||||
from coolrl_lost_cities.games.classic.bots import build_bot
|
from coolrl_lost_cities.games.classic.bots import build_bot
|
||||||
from coolrl_lost_cities.games.classic.deep_cfr.config import config_from_dict
|
from coolrl_lost_cities.games.classic.deep_cfr.config import EncodingConfig, config_from_dict
|
||||||
from coolrl_lost_cities.games.classic.deep_cfr.encoding import encode_info_state
|
from coolrl_lost_cities.games.classic.deep_cfr.encoding import encode_info_state
|
||||||
from coolrl_lost_cities.games.classic.deep_cfr.networks import DeepCFRMLP
|
from coolrl_lost_cities.games.classic.deep_cfr.networks import DeepCFRMLP
|
||||||
from coolrl_lost_cities.games.classic.evaluation import evaluate_policy
|
from coolrl_lost_cities.games.classic.evaluation import evaluate_policy
|
||||||
@@ -22,11 +22,13 @@ class StrategyNetPolicy(LostCitiesPolicy):
|
|||||||
device: torch.device | str = "cpu",
|
device: torch.device | str = "cpu",
|
||||||
sample: bool = False,
|
sample: bool = False,
|
||||||
seed: int | None = None,
|
seed: int | None = None,
|
||||||
|
encoding: EncodingConfig | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.strategy_network = strategy_network
|
self.strategy_network = strategy_network
|
||||||
self.device = torch.device(device)
|
self.device = torch.device(device)
|
||||||
self.sample = sample
|
self.sample = sample
|
||||||
self.rng = np.random.default_rng(seed)
|
self.rng = np.random.default_rng(seed)
|
||||||
|
self.encoding = encoding
|
||||||
|
|
||||||
def act(self, obs_or_state: PolicyInput) -> int:
|
def act(self, obs_or_state: PolicyInput) -> int:
|
||||||
if not isinstance(obs_or_state, GameState):
|
if not isinstance(obs_or_state, GameState):
|
||||||
@@ -40,7 +42,7 @@ class StrategyNetPolicy(LostCitiesPolicy):
|
|||||||
legal_actions = np.flatnonzero(legal)
|
legal_actions = np.flatnonzero(legal)
|
||||||
if len(legal_actions) == 0:
|
if len(legal_actions) == 0:
|
||||||
raise RuntimeError("no legal action available")
|
raise RuntimeError("no legal action available")
|
||||||
info = encode_info_state(state, state.current_player)
|
info = encode_info_state(state, state.current_player, self.encoding)
|
||||||
with torch.inference_mode():
|
with torch.inference_mode():
|
||||||
x = torch.as_tensor(info, dtype=torch.float32, device=self.device).unsqueeze(0)
|
x = torch.as_tensor(info, dtype=torch.float32, device=self.device).unsqueeze(0)
|
||||||
logits = self.strategy_network(x).squeeze(0).detach().cpu().numpy()
|
logits = self.strategy_network(x).squeeze(0).detach().cpu().numpy()
|
||||||
@@ -64,11 +66,14 @@ def evaluate_strategy_network(
|
|||||||
opponent: str = "random",
|
opponent: str = "random",
|
||||||
device: torch.device | str = "cpu",
|
device: torch.device | str = "cpu",
|
||||||
max_steps: int = 10_000,
|
max_steps: int = 10_000,
|
||||||
|
encoding: EncodingConfig | None = None,
|
||||||
) -> dict[str, float | int]:
|
) -> dict[str, float | int]:
|
||||||
strategy_network.eval()
|
strategy_network.eval()
|
||||||
|
|
||||||
def make_strategy(seed_value: int | None = None) -> StrategyNetPolicy:
|
def make_strategy(seed_value: int | None = None) -> StrategyNetPolicy:
|
||||||
return StrategyNetPolicy(strategy_network, device=device, seed=seed_value)
|
return StrategyNetPolicy(
|
||||||
|
strategy_network, device=device, seed=seed_value, encoding=encoding
|
||||||
|
)
|
||||||
|
|
||||||
def opponent_factory(seed_value=None):
|
def opponent_factory(seed_value=None):
|
||||||
return build_bot(opponent, seed=seed_value)
|
return build_bot(opponent, seed=seed_value)
|
||||||
@@ -101,4 +106,7 @@ def load_strategy_policy_from_checkpoint(
|
|||||||
).to(device)
|
).to(device)
|
||||||
network.load_state_dict(payload["strategy_network"])
|
network.load_state_dict(payload["strategy_network"])
|
||||||
network.eval()
|
network.eval()
|
||||||
return StrategyNetPolicy(network, device=device, sample=sample, seed=seed), game_config
|
return (
|
||||||
|
StrategyNetPolicy(network, device=device, sample=sample, seed=seed, encoding=cfg.encoding),
|
||||||
|
game_config,
|
||||||
|
)
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class DeepCFRTrainer:
|
|||||||
self.device = _resolve_torch_device(device)
|
self.device = _resolve_torch_device(device)
|
||||||
|
|
||||||
probe = GameState.new_game(self.game_config, seed=self.config.run.seed)
|
probe = GameState.new_game(self.game_config, seed=self.config.run.seed)
|
||||||
self.input_dim = input_dim(probe)
|
self.input_dim = input_dim(probe, self.config.encoding)
|
||||||
self.action_size = 2 * probe.config.hand_size + 1 + probe.config.n_colors
|
self.action_size = 2 * probe.config.hand_size + 1 + probe.config.n_colors
|
||||||
|
|
||||||
torch.manual_seed(self.config.run.seed)
|
torch.manual_seed(self.config.run.seed)
|
||||||
@@ -206,6 +206,7 @@ class DeepCFRTrainer:
|
|||||||
self_play_older_weight=self.config.self_play.older_weight,
|
self_play_older_weight=self.config.self_play.older_weight,
|
||||||
self_play_anchor_weight=self.config.self_play.anchor_weight,
|
self_play_anchor_weight=self.config.self_play.anchor_weight,
|
||||||
self_play_recent_window=self.config.self_play.recent_window,
|
self_play_recent_window=self.config.self_play.recent_window,
|
||||||
|
encoding=self.config.encoding,
|
||||||
rng=self.rng,
|
rng=self.rng,
|
||||||
)
|
)
|
||||||
for network in self.advantage_networks:
|
for network in self.advantage_networks:
|
||||||
@@ -394,6 +395,7 @@ class DeepCFRTrainer:
|
|||||||
opponent=opponent,
|
opponent=opponent,
|
||||||
device=self.device,
|
device=self.device,
|
||||||
max_steps=self.config.evaluation.max_steps,
|
max_steps=self.config.evaluation.max_steps,
|
||||||
|
encoding=self.config.encoding,
|
||||||
)
|
)
|
||||||
for key, value in result.items():
|
for key, value in result.items():
|
||||||
results[f"eval_{opponent}_{key}"] = value
|
results[f"eval_{opponent}_{key}"] = value
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ class DeepCFRTraverser:
|
|||||||
self_play_older_weight: float = 0.2,
|
self_play_older_weight: float = 0.2,
|
||||||
self_play_anchor_weight: float = 0.0,
|
self_play_anchor_weight: float = 0.0,
|
||||||
self_play_recent_window: int = 5,
|
self_play_recent_window: int = 5,
|
||||||
|
encoding=None,
|
||||||
rng: np.random.Generator | None = None,
|
rng: np.random.Generator | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.advantage_networks = advantage_networks
|
self.advantage_networks = advantage_networks
|
||||||
@@ -145,6 +146,7 @@ class DeepCFRTraverser:
|
|||||||
self.self_play_older_weight = max(0.0, float(self_play_older_weight))
|
self.self_play_older_weight = max(0.0, float(self_play_older_weight))
|
||||||
self.self_play_anchor_weight = max(0.0, float(self_play_anchor_weight))
|
self.self_play_anchor_weight = max(0.0, float(self_play_anchor_weight))
|
||||||
self.self_play_recent_window = max(0, int(self_play_recent_window))
|
self.self_play_recent_window = max(0, int(self_play_recent_window))
|
||||||
|
self.encoding = encoding
|
||||||
self.rng = rng or np.random.default_rng()
|
self.rng = rng or np.random.default_rng()
|
||||||
self._safe_heuristic_rollout_bot = (
|
self._safe_heuristic_rollout_bot = (
|
||||||
SafeHeuristicBot() if self.cutoff_rollout_policy == "safe_heuristic" else None
|
SafeHeuristicBot() if self.cutoff_rollout_policy == "safe_heuristic" else None
|
||||||
@@ -275,7 +277,7 @@ class DeepCFRTraverser:
|
|||||||
state: GameState,
|
state: GameState,
|
||||||
player: int,
|
player: int,
|
||||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||||
info_state = encode_info_state(state, player)
|
info_state = encode_info_state(state, player, self.encoding)
|
||||||
legal = np.asarray(state.unified_legal_mask(), dtype=bool)
|
legal = np.asarray(state.unified_legal_mask(), dtype=bool)
|
||||||
with torch.inference_mode():
|
with torch.inference_mode():
|
||||||
x = torch.as_tensor(info_state, dtype=torch.float32, device=self.device).unsqueeze(0)
|
x = torch.as_tensor(info_state, dtype=torch.float32, device=self.device).unsqueeze(0)
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ def run_traversal_worker_batch(batch: TraversalWorkerBatch) -> TraversalWorkerRe
|
|||||||
self_play_older_weight=cfg.self_play.older_weight,
|
self_play_older_weight=cfg.self_play.older_weight,
|
||||||
self_play_anchor_weight=cfg.self_play.anchor_weight,
|
self_play_anchor_weight=cfg.self_play.anchor_weight,
|
||||||
self_play_recent_window=cfg.self_play.recent_window,
|
self_play_recent_window=cfg.self_play.recent_window,
|
||||||
|
encoding=cfg.encoding,
|
||||||
rng=np.random.default_rng(batch.worker_seed),
|
rng=np.random.default_rng(batch.worker_seed),
|
||||||
)
|
)
|
||||||
game_config = LostCitiesConfig(**batch.game_config)
|
game_config = LostCitiesConfig(**batch.game_config)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from coolrl_lost_cities.games.classic.deep_cfr.encoding import encode_info_state, input_dim
|
||||||
from coolrl_lost_cities.games.classic.game import GameState, LostCitiesConfig
|
from coolrl_lost_cities.games.classic.game import GameState, LostCitiesConfig
|
||||||
|
|
||||||
from coolrl_lost_cities.games.classic.deep_cfr.benchmark import (
|
from coolrl_lost_cities.games.classic.deep_cfr.benchmark import (
|
||||||
@@ -53,6 +54,47 @@ def test_deep_cfr_loads_mapped_legacy_reproduction_config() -> None:
|
|||||||
assert config.checkpoint.save_iteration_interval == 10
|
assert config.checkpoint.save_iteration_interval == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_deep_cfr_playability_encoding_extends_input_shape() -> None:
|
||||||
|
state = GameState.new_game(LostCitiesConfig(seed=61), seed=61)
|
||||||
|
base_dim = input_dim(state)
|
||||||
|
derived_config = _deep_cfr_config({"encoding": {"derived_playability": True}})
|
||||||
|
slot_config = _deep_cfr_config(
|
||||||
|
{"encoding": {"derived_playability": True, "slot_aware_playability": True}}
|
||||||
|
)
|
||||||
|
|
||||||
|
derived_dim = input_dim(state, derived_config.encoding)
|
||||||
|
slot_dim = input_dim(state, slot_config.encoding)
|
||||||
|
|
||||||
|
assert derived_dim == base_dim + state.config.n_colors * 19 + 3
|
||||||
|
assert slot_dim == derived_dim + state.config.hand_size * 12
|
||||||
|
assert encode_info_state(state, 0, slot_config.encoding).shape == (slot_dim,)
|
||||||
|
|
||||||
|
|
||||||
|
def test_deep_cfr_trainer_uses_playability_encoding() -> None:
|
||||||
|
config = _deep_cfr_config(
|
||||||
|
{
|
||||||
|
"run": {"iterations": 1, "seed": 62},
|
||||||
|
"encoding": {"derived_playability": True, "slot_aware_playability": True},
|
||||||
|
"network": {"hidden_size": 16},
|
||||||
|
"traversal": {"traversals_per_iteration": 1, "max_depth": 1, "max_nodes": 16},
|
||||||
|
"optimization": {
|
||||||
|
"advantage_train_steps": 1,
|
||||||
|
"strategy_train_steps": 1,
|
||||||
|
"batch_size": 2,
|
||||||
|
},
|
||||||
|
"checkpoint": {"save_every_iteration": False},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
game_config = LostCitiesConfig(seed=62)
|
||||||
|
trainer = DeepCFRTrainer(config, game_config)
|
||||||
|
|
||||||
|
metrics = trainer.train()
|
||||||
|
|
||||||
|
probe = GameState.new_game(game_config, seed=62)
|
||||||
|
assert trainer.input_dim == input_dim(probe, config.encoding)
|
||||||
|
assert metrics[0].advantage_samples > 0
|
||||||
|
|
||||||
|
|
||||||
def test_deep_cfr_trainer_smoke_run() -> None:
|
def test_deep_cfr_trainer_smoke_run() -> None:
|
||||||
trainer = DeepCFRTrainer(
|
trainer = DeepCFRTrainer(
|
||||||
_deep_cfr_config(
|
_deep_cfr_config(
|
||||||
|
|||||||
Reference in New Issue
Block a user