diff --git a/src/coolrl_lost_cities/games/classic/deep_cfr/encoding.pyx b/src/coolrl_lost_cities/games/classic/deep_cfr/encoding.pyx index 9a285fa..c591882 100644 --- a/src/coolrl_lost_cities/games/classic/deep_cfr/encoding.pyx +++ b/src/coolrl_lost_cities/games/classic/deep_cfr/encoding.pyx @@ -1,11 +1,23 @@ # cython: language_level=3, boundscheck=False, wraparound=False, cdivision=True, initializedcheck=False -"""Minimal deterministic information-state encoding for Deep CFR scaffolding.""" +"""Deterministic information-state encoding for Deep CFR.""" from coolrl_lost_cities.games.classic.game cimport GameState cdef int input_dim_c(GameState state) noexcept: - return 5 + state.hand_size * 3 + 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) + return ( + 5 + + state.hand_size * 3 + + 2 * state.n_colors * 4 + + state.n_colors * 4 + + card_type_size + + 3 + + 1 + + state.n_colors + 1 + + action_size + ) cdef int encode_info_state_c(GameState state, int player, float* out) except -1: @@ -15,6 +27,16 @@ cdef int encode_info_state_c(GameState state, int player, float* out) except -1: cdef int color cdef int rank 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 public_base + cdef int player_index + cdef int length + cdef int top_card + cdef int card_index + cdef int max_expedition_len = state.n_ranks + state.n_handshakes + cdef float score_denom = ( + max(1, state.bonus_amount + (state.n_ranks * (state.n_ranks + 1)) * (state.n_handshakes + 1)) + ) cdef int action_count cdef int actions[64] cdef int i @@ -49,6 +71,61 @@ cdef int encode_info_state_c(GameState state, int player, float* out) except -1: out[idx + 2] = 0.0 idx += 3 + for player_index in range(2): + for color in range(state.n_colors): + card_index = state._expedition_len_index(player_index, color) + length = state.expedition_lens[card_index] + out[idx] = length / max_expedition_len + out[idx + 1] = state.last_numeric_ranks[card_index] / state.n_ranks + out[idx + 2] = state.handshake_counts[card_index] / max(1, state.n_handshakes) + out[idx + 3] = state.expedition_scores[card_index] / score_denom + idx += 4 + + for color in range(state.n_colors): + length = state.discard_lens[color] + out[idx] = length / max_expedition_len + if length > 0: + top_card = state.discard_cards[state._discard_index(color, length - 1)] + out[idx + 1] = 1.0 + out[idx + 2] = (state._card_color(top_card) + 1) / state.n_colors + out[idx + 3] = state._card_rank(top_card) / state.n_ranks + else: + out[idx + 1] = 0.0 + out[idx + 2] = 0.0 + out[idx + 3] = 0.0 + idx += 4 + + public_base = idx + for i in range(card_type_size): + out[public_base + i] = 0.0 + for player_index in range(2): + for color in range(state.n_colors): + card_index = state._expedition_len_index(player_index, color) + for i in range(state.expedition_lens[card_index]): + card = state.expedition_cards[state._expedition_index(player_index, color, i)] + out[public_base + state._card_color(card) * (state.n_ranks + 1) + state._card_rank(card)] += 1.0 + for color in range(state.n_colors): + for i in range(state.discard_lens[color]): + card = state.discard_cards[state._discard_index(color, i)] + out[public_base + state._card_color(card) * (state.n_ranks + 1) + state._card_rank(card)] += 1.0 + idx += card_type_size + + out[idx] = state.total_scores[player] / score_denom + out[idx + 1] = state.total_scores[1 - player] / score_denom + out[idx + 2] = (state.total_scores[player] - state.total_scores[1 - player]) / score_denom + idx += 3 + + out[idx] = state.turn_count / max(1, state.total_cards * 2) + idx += 1 + + for i in range(state.n_colors + 1): + out[idx + i] = 0.0 + if state.pending_discarded_color < 0: + out[idx + state.n_colors] = 1.0 + else: + out[idx + state.pending_discarded_color] = 1.0 + idx += state.n_colors + 1 + for i in range(action_size): out[idx + i] = 0.0 action_count = state._unified_legal_actions_c(actions) diff --git a/tests/games/classic/test_deep_cfr_foundation.py b/tests/games/classic/test_deep_cfr_foundation.py index d4a8df3..baed768 100644 --- a/tests/games/classic/test_deep_cfr_foundation.py +++ b/tests/games/classic/test_deep_cfr_foundation.py @@ -92,6 +92,7 @@ def test_encode_info_state_is_deterministic_and_matches_legal_mask_tail() -> Non assert encoded.dtype == np.float32 assert encoded.shape == (input_dim(state),) + assert input_dim(state) > 5 + state.config.hand_size * 3 + len(legal_mask) np.testing.assert_array_equal(encoded, encoded_again) np.testing.assert_array_equal(encoded[-len(legal_mask) :], legal_mask)