고속 엔진 hot path 최적화
This commit is contained in:
@@ -17,6 +17,24 @@ cdef inline int _phase_draw():
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
ctypedef struct UndoRecord:
|
||||||
|
int phase_id
|
||||||
|
int player
|
||||||
|
int action_id
|
||||||
|
int pending_before
|
||||||
|
bint terminal_before
|
||||||
|
int turn_count_before
|
||||||
|
int slot
|
||||||
|
int play
|
||||||
|
int card
|
||||||
|
int color
|
||||||
|
int last_numeric_before
|
||||||
|
int handshake_count_before
|
||||||
|
int numeric_sum_before
|
||||||
|
int expedition_score_before
|
||||||
|
int total_score_before
|
||||||
|
|
||||||
|
|
||||||
cdef class FastGameState:
|
cdef class FastGameState:
|
||||||
cdef public object config
|
cdef public object config
|
||||||
cdef int n_colors
|
cdef int n_colors
|
||||||
@@ -39,6 +57,11 @@ cdef class FastGameState:
|
|||||||
cdef int* expedition_lens
|
cdef int* expedition_lens
|
||||||
cdef int* discards
|
cdef int* discards
|
||||||
cdef int* discard_lens
|
cdef int* discard_lens
|
||||||
|
cdef int* last_numeric_ranks
|
||||||
|
cdef int* handshake_counts
|
||||||
|
cdef int* numeric_sums
|
||||||
|
cdef int* expedition_scores
|
||||||
|
cdef int total_scores[2]
|
||||||
|
|
||||||
cdef public int current_player
|
cdef public int current_player
|
||||||
cdef int phase_id
|
cdef int phase_id
|
||||||
@@ -53,6 +76,10 @@ cdef class FastGameState:
|
|||||||
self.expedition_lens = NULL
|
self.expedition_lens = NULL
|
||||||
self.discards = NULL
|
self.discards = NULL
|
||||||
self.discard_lens = NULL
|
self.discard_lens = NULL
|
||||||
|
self.last_numeric_ranks = NULL
|
||||||
|
self.handshake_counts = NULL
|
||||||
|
self.numeric_sums = NULL
|
||||||
|
self.expedition_scores = NULL
|
||||||
|
|
||||||
def __init__(self, config=None):
|
def __init__(self, config=None):
|
||||||
config = config or LostCitiesConfig()
|
config = config or LostCitiesConfig()
|
||||||
@@ -72,6 +99,14 @@ cdef class FastGameState:
|
|||||||
free(self.discards)
|
free(self.discards)
|
||||||
if self.discard_lens != NULL:
|
if self.discard_lens != NULL:
|
||||||
free(self.discard_lens)
|
free(self.discard_lens)
|
||||||
|
if self.last_numeric_ranks != NULL:
|
||||||
|
free(self.last_numeric_ranks)
|
||||||
|
if self.handshake_counts != NULL:
|
||||||
|
free(self.handshake_counts)
|
||||||
|
if self.numeric_sums != NULL:
|
||||||
|
free(self.numeric_sums)
|
||||||
|
if self.expedition_scores != NULL:
|
||||||
|
free(self.expedition_scores)
|
||||||
|
|
||||||
cdef void _configure(self, object config) except *:
|
cdef void _configure(self, object config) except *:
|
||||||
self.config = config
|
self.config = config
|
||||||
@@ -95,6 +130,10 @@ cdef class FastGameState:
|
|||||||
self.expedition_lens = <int*>malloc(2 * self.n_colors * sizeof(int))
|
self.expedition_lens = <int*>malloc(2 * self.n_colors * sizeof(int))
|
||||||
self.discards = <int*>malloc(self.n_colors * self.cards_per_color * sizeof(int))
|
self.discards = <int*>malloc(self.n_colors * self.cards_per_color * sizeof(int))
|
||||||
self.discard_lens = <int*>malloc(self.n_colors * sizeof(int))
|
self.discard_lens = <int*>malloc(self.n_colors * sizeof(int))
|
||||||
|
self.last_numeric_ranks = <int*>malloc(2 * self.n_colors * sizeof(int))
|
||||||
|
self.handshake_counts = <int*>malloc(2 * self.n_colors * sizeof(int))
|
||||||
|
self.numeric_sums = <int*>malloc(2 * self.n_colors * sizeof(int))
|
||||||
|
self.expedition_scores = <int*>malloc(2 * self.n_colors * sizeof(int))
|
||||||
if (
|
if (
|
||||||
self.deck == NULL
|
self.deck == NULL
|
||||||
or self.hands == NULL
|
or self.hands == NULL
|
||||||
@@ -102,6 +141,10 @@ cdef class FastGameState:
|
|||||||
or self.expedition_lens == NULL
|
or self.expedition_lens == NULL
|
||||||
or self.discards == NULL
|
or self.discards == NULL
|
||||||
or self.discard_lens == NULL
|
or self.discard_lens == NULL
|
||||||
|
or self.last_numeric_ranks == NULL
|
||||||
|
or self.handshake_counts == NULL
|
||||||
|
or self.numeric_sums == NULL
|
||||||
|
or self.expedition_scores == NULL
|
||||||
):
|
):
|
||||||
raise MemoryError()
|
raise MemoryError()
|
||||||
self._clear()
|
self._clear()
|
||||||
@@ -113,8 +156,14 @@ cdef class FastGameState:
|
|||||||
self.hand_lens[1] = 0
|
self.hand_lens[1] = 0
|
||||||
for i in range(2 * self.n_colors):
|
for i in range(2 * self.n_colors):
|
||||||
self.expedition_lens[i] = 0
|
self.expedition_lens[i] = 0
|
||||||
|
self.last_numeric_ranks[i] = 0
|
||||||
|
self.handshake_counts[i] = 0
|
||||||
|
self.numeric_sums[i] = 0
|
||||||
|
self.expedition_scores[i] = 0
|
||||||
for i in range(self.n_colors):
|
for i in range(self.n_colors):
|
||||||
self.discard_lens[i] = 0
|
self.discard_lens[i] = 0
|
||||||
|
self.total_scores[0] = 0
|
||||||
|
self.total_scores[1] = 0
|
||||||
self.current_player = 0
|
self.current_player = 0
|
||||||
self.phase_id = _phase_card()
|
self.phase_id = _phase_card()
|
||||||
self.pending_discarded_color = -1
|
self.pending_discarded_color = -1
|
||||||
@@ -202,6 +251,7 @@ cdef class FastGameState:
|
|||||||
state.pending_discarded_color = -1 if pending is None else int(pending)
|
state.pending_discarded_color = -1 if pending is None else int(pending)
|
||||||
state.turn_count = int(snapshot.get("turn_count", 0))
|
state.turn_count = int(snapshot.get("turn_count", 0))
|
||||||
state.terminal = bool(snapshot.get("terminal", False))
|
state.terminal = bool(snapshot.get("terminal", False))
|
||||||
|
state._recompute_score_caches()
|
||||||
if validate:
|
if validate:
|
||||||
state.validate_invariants()
|
state.validate_invariants()
|
||||||
return state
|
return state
|
||||||
@@ -288,10 +338,16 @@ cdef class FastGameState:
|
|||||||
other.expeditions[i] = self.expeditions[i]
|
other.expeditions[i] = self.expeditions[i]
|
||||||
for i in range(2 * self.n_colors):
|
for i in range(2 * self.n_colors):
|
||||||
other.expedition_lens[i] = self.expedition_lens[i]
|
other.expedition_lens[i] = self.expedition_lens[i]
|
||||||
|
other.last_numeric_ranks[i] = self.last_numeric_ranks[i]
|
||||||
|
other.handshake_counts[i] = self.handshake_counts[i]
|
||||||
|
other.numeric_sums[i] = self.numeric_sums[i]
|
||||||
|
other.expedition_scores[i] = self.expedition_scores[i]
|
||||||
for i in range(self.n_colors * self.cards_per_color):
|
for i in range(self.n_colors * self.cards_per_color):
|
||||||
other.discards[i] = self.discards[i]
|
other.discards[i] = self.discards[i]
|
||||||
for i in range(self.n_colors):
|
for i in range(self.n_colors):
|
||||||
other.discard_lens[i] = self.discard_lens[i]
|
other.discard_lens[i] = self.discard_lens[i]
|
||||||
|
other.total_scores[0] = self.total_scores[0]
|
||||||
|
other.total_scores[1] = self.total_scores[1]
|
||||||
other.current_player = self.current_player
|
other.current_player = self.current_player
|
||||||
other.phase_id = self.phase_id
|
other.phase_id = self.phase_id
|
||||||
other.pending_discarded_color = self.pending_discarded_color
|
other.pending_discarded_color = self.pending_discarded_color
|
||||||
@@ -339,6 +395,30 @@ cdef class FastGameState:
|
|||||||
result.extend(self.legal_draw_mask())
|
result.extend(self.legal_draw_mask())
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
cpdef list legal_actions(self):
|
||||||
|
cdef int* actions = <int*>malloc(self.action_size * sizeof(int))
|
||||||
|
if actions == NULL:
|
||||||
|
raise MemoryError()
|
||||||
|
cdef int count
|
||||||
|
cdef int i
|
||||||
|
try:
|
||||||
|
count = self._legal_actions_c(actions)
|
||||||
|
return [actions[i] for i in range(count)]
|
||||||
|
finally:
|
||||||
|
free(actions)
|
||||||
|
|
||||||
|
cpdef list unified_legal_actions(self):
|
||||||
|
cdef int* actions = <int*>malloc(self.action_size * sizeof(int))
|
||||||
|
if actions == NULL:
|
||||||
|
raise MemoryError()
|
||||||
|
cdef int count
|
||||||
|
cdef int i
|
||||||
|
try:
|
||||||
|
count = self._unified_legal_actions_c(actions)
|
||||||
|
return [actions[i] for i in range(count)]
|
||||||
|
finally:
|
||||||
|
free(actions)
|
||||||
|
|
||||||
cpdef int from_unified_action(self, int action_id):
|
cpdef int from_unified_action(self, int action_id):
|
||||||
cdef int card_action_size = 2 * self.hand_size
|
cdef int card_action_size = 2 * self.hand_size
|
||||||
cdef int action_size = card_action_size + 1 + self.n_colors
|
cdef int action_size = card_action_size + 1 + self.n_colors
|
||||||
@@ -369,16 +449,12 @@ cdef class FastGameState:
|
|||||||
cpdef apply_action(self, int action_id):
|
cpdef apply_action(self, int action_id):
|
||||||
if self.terminal:
|
if self.terminal:
|
||||||
raise IllegalMoveError("game is already terminal")
|
raise IllegalMoveError("game is already terminal")
|
||||||
cdef list mask = self.legal_mask()
|
if not self._is_legal_action_c(action_id):
|
||||||
if action_id < 0 or action_id >= len(mask) or not mask[action_id]:
|
|
||||||
raise IllegalMoveError(
|
raise IllegalMoveError(
|
||||||
f"illegal action {action_id} in phase {self.phase} "
|
f"illegal action {action_id} in phase {self.phase} "
|
||||||
f"for player {self.current_player}"
|
f"for player {self.current_player}"
|
||||||
)
|
)
|
||||||
if self.phase_id == _phase_card():
|
self._apply_action_unchecked_c(action_id)
|
||||||
self._apply_card_action(action_id)
|
|
||||||
else:
|
|
||||||
self._apply_draw_action(action_id)
|
|
||||||
|
|
||||||
cpdef apply_unified_action(self, int action_id):
|
cpdef apply_unified_action(self, int action_id):
|
||||||
self.apply_action(self.from_unified_action(action_id))
|
self.apply_action(self.from_unified_action(action_id))
|
||||||
@@ -386,84 +462,42 @@ cdef class FastGameState:
|
|||||||
cpdef object apply_action_with_undo(self, int action_id):
|
cpdef object apply_action_with_undo(self, int action_id):
|
||||||
if self.terminal:
|
if self.terminal:
|
||||||
raise IllegalMoveError("game is already terminal")
|
raise IllegalMoveError("game is already terminal")
|
||||||
cdef list mask = self.legal_mask()
|
if not self._is_legal_action_c(action_id):
|
||||||
if action_id < 0 or action_id >= len(mask) or not mask[action_id]:
|
|
||||||
raise IllegalMoveError(
|
raise IllegalMoveError(
|
||||||
f"illegal action {action_id} in phase {self.phase} "
|
f"illegal action {action_id} in phase {self.phase} "
|
||||||
f"for player {self.current_player}"
|
f"for player {self.current_player}"
|
||||||
)
|
)
|
||||||
cdef object undo
|
cdef UndoRecord undo
|
||||||
if self.phase_id == _phase_card():
|
self._apply_action_with_undo_c(action_id, &undo)
|
||||||
undo = self._card_action_undo(action_id)
|
return self._undo_to_tuple(&undo)
|
||||||
self._apply_card_action(action_id)
|
|
||||||
else:
|
|
||||||
undo = self._draw_action_undo(action_id)
|
|
||||||
self._apply_draw_action(action_id)
|
|
||||||
return undo
|
|
||||||
|
|
||||||
cpdef object apply_unified_action_with_undo(self, int action_id):
|
cpdef object apply_unified_action_with_undo(self, int action_id):
|
||||||
return self.apply_action_with_undo(self.from_unified_action(action_id))
|
return self.apply_action_with_undo(self.from_unified_action(action_id))
|
||||||
|
|
||||||
cpdef undo_action(self, object undo):
|
cpdef undo_action(self, object undo):
|
||||||
cdef str phase = undo[0]
|
cdef UndoRecord record
|
||||||
if phase == "card":
|
self._tuple_to_undo(undo, &record)
|
||||||
self._undo_card_action(undo)
|
self._undo_action_c(&record)
|
||||||
return
|
|
||||||
if phase == "draw":
|
|
||||||
self._undo_draw_action(undo)
|
|
||||||
return
|
|
||||||
raise ValueError(f"invalid undo phase: {phase!r}")
|
|
||||||
|
|
||||||
cpdef bint can_play_encoded_card(self, int player, int card):
|
cpdef bint can_play_encoded_card(self, int player, int card):
|
||||||
cdef int color = self._card_color(card)
|
cdef int color = self._card_color(card)
|
||||||
cdef int rank = self._card_rank(card)
|
cdef int rank = self._card_rank(card)
|
||||||
cdef int last_numeric
|
|
||||||
if color < 0 or color >= self.n_colors:
|
if color < 0 or color >= self.n_colors:
|
||||||
return False
|
return False
|
||||||
if rank < 0 or rank > self.n_ranks:
|
if rank < 0 or rank > self.n_ranks:
|
||||||
return False
|
return False
|
||||||
last_numeric = self.last_numeric_rank(player, color)
|
|
||||||
if rank == 0:
|
if rank == 0:
|
||||||
return last_numeric == 0
|
return self.last_numeric_ranks[self._expedition_len_index(player, color)] == 0
|
||||||
return rank > last_numeric
|
return rank > self.last_numeric_ranks[self._expedition_len_index(player, color)]
|
||||||
|
|
||||||
cpdef int last_numeric_rank(self, int player, int color):
|
cpdef int last_numeric_rank(self, int player, int color):
|
||||||
cdef int length = self.expedition_lens[self._expedition_len_index(player, color)]
|
return self.last_numeric_ranks[self._expedition_len_index(player, color)]
|
||||||
cdef int i
|
|
||||||
cdef int rank
|
|
||||||
cdef int best = 0
|
|
||||||
for i in range(length):
|
|
||||||
rank = self._card_rank(self.expeditions[self._expedition_index(player, color, i)])
|
|
||||||
if rank > best:
|
|
||||||
best = rank
|
|
||||||
return best
|
|
||||||
|
|
||||||
cpdef int expedition_score(self, int player, int color):
|
cpdef int expedition_score(self, int player, int color):
|
||||||
cdef int length = self.expedition_lens[self._expedition_len_index(player, color)]
|
return self.expedition_scores[self._expedition_len_index(player, color)]
|
||||||
cdef int handshakes = 0
|
|
||||||
cdef int numeric_sum = 0
|
|
||||||
cdef int i
|
|
||||||
cdef int rank
|
|
||||||
cdef int score
|
|
||||||
if length == 0:
|
|
||||||
return 0
|
|
||||||
for i in range(length):
|
|
||||||
rank = self._card_rank(self.expeditions[self._expedition_index(player, color, i)])
|
|
||||||
if rank == 0:
|
|
||||||
handshakes += 1
|
|
||||||
else:
|
|
||||||
numeric_sum += self.min_rank + rank - 1
|
|
||||||
score = (numeric_sum + self.expedition_penalty) * (handshakes + 1)
|
|
||||||
if length >= self.bonus_threshold:
|
|
||||||
score += self.bonus_amount
|
|
||||||
return score
|
|
||||||
|
|
||||||
cpdef int total_score(self, int player):
|
cpdef int total_score(self, int player):
|
||||||
cdef int total = 0
|
return self.total_scores[player]
|
||||||
cdef int color
|
|
||||||
for color in range(self.n_colors):
|
|
||||||
total += self.expedition_score(player, color)
|
|
||||||
return total
|
|
||||||
|
|
||||||
cpdef int score_diff(self, int player=0):
|
cpdef int score_diff(self, int player=0):
|
||||||
return self.total_score(player) - self.total_score(1 - player)
|
return self.total_score(player) - self.total_score(1 - player)
|
||||||
@@ -492,52 +526,224 @@ cdef class FastGameState:
|
|||||||
if not self.terminal and not any_legal:
|
if not self.terminal and not any_legal:
|
||||||
raise ValueError("non-terminal state must have at least one legal action")
|
raise ValueError("non-terminal state must have at least one legal action")
|
||||||
|
|
||||||
cdef object _card_action_undo(self, int action_id):
|
cdef bint _is_legal_action_c(self, int action_id) noexcept:
|
||||||
cdef int slot = action_id // 2
|
cdef int slot
|
||||||
cdef bint play = action_id % 2 == 0
|
cdef int color
|
||||||
cdef int card = self.hands[self._hand_index(self.current_player, slot)]
|
if self.terminal:
|
||||||
return (
|
return False
|
||||||
"card",
|
if self.phase_id == _phase_card():
|
||||||
|
if action_id < 0 or action_id >= 2 * self.hand_size:
|
||||||
|
return False
|
||||||
|
slot = action_id // 2
|
||||||
|
if slot >= self.hand_lens[self.current_player]:
|
||||||
|
return False
|
||||||
|
if action_id % 2 == 1:
|
||||||
|
return True
|
||||||
|
return self._can_play_encoded_card_c(
|
||||||
self.current_player,
|
self.current_player,
|
||||||
action_id,
|
self.hands[self._hand_index(self.current_player, slot)],
|
||||||
self.pending_discarded_color,
|
)
|
||||||
self.terminal,
|
if action_id < 0 or action_id >= 1 + self.n_colors:
|
||||||
slot,
|
return False
|
||||||
play,
|
if action_id == 0:
|
||||||
card,
|
return self.deck_len > 0
|
||||||
|
color = action_id - 1
|
||||||
|
return (
|
||||||
|
self.discard_lens[color] > 0
|
||||||
|
and (self.pending_discarded_color < 0 or color != self.pending_discarded_color)
|
||||||
)
|
)
|
||||||
|
|
||||||
cdef object _draw_action_undo(self, int action_id):
|
cdef int _legal_actions_c(self, int* out_actions) noexcept:
|
||||||
|
cdef int count = 0
|
||||||
|
cdef int slot
|
||||||
|
cdef int color
|
||||||
cdef int card
|
cdef int card
|
||||||
if action_id == 0:
|
if self.terminal:
|
||||||
card = self.deck[self.deck_len - 1]
|
return 0
|
||||||
|
if self.phase_id == _phase_card():
|
||||||
|
for slot in range(self.hand_lens[self.current_player]):
|
||||||
|
card = self.hands[self._hand_index(self.current_player, slot)]
|
||||||
|
if self._can_play_encoded_card_c(self.current_player, card):
|
||||||
|
out_actions[count] = 2 * slot
|
||||||
|
count += 1
|
||||||
|
out_actions[count] = 2 * slot + 1
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
if self.deck_len > 0:
|
||||||
|
out_actions[count] = 0
|
||||||
|
count += 1
|
||||||
|
for color in range(self.n_colors):
|
||||||
|
if (
|
||||||
|
self.discard_lens[color] > 0
|
||||||
|
and (self.pending_discarded_color < 0 or color != self.pending_discarded_color)
|
||||||
|
):
|
||||||
|
out_actions[count] = 1 + color
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
cdef int _unified_legal_actions_c(self, int* out_actions) noexcept:
|
||||||
|
cdef int count = 0
|
||||||
|
cdef int slot
|
||||||
|
cdef int color
|
||||||
|
cdef int card
|
||||||
|
cdef int card_action_size = 2 * self.hand_size
|
||||||
|
if self.terminal:
|
||||||
|
return 0
|
||||||
|
if self.phase_id == _phase_card():
|
||||||
|
for slot in range(self.hand_lens[self.current_player]):
|
||||||
|
card = self.hands[self._hand_index(self.current_player, slot)]
|
||||||
|
if self._can_play_encoded_card_c(self.current_player, card):
|
||||||
|
out_actions[count] = 2 * slot
|
||||||
|
count += 1
|
||||||
|
out_actions[count] = 2 * slot + 1
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
if self.deck_len > 0:
|
||||||
|
out_actions[count] = card_action_size
|
||||||
|
count += 1
|
||||||
|
for color in range(self.n_colors):
|
||||||
|
if (
|
||||||
|
self.discard_lens[color] > 0
|
||||||
|
and (self.pending_discarded_color < 0 or color != self.pending_discarded_color)
|
||||||
|
):
|
||||||
|
out_actions[count] = card_action_size + 1 + color
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
cdef bint _can_play_encoded_card_c(self, int player, int card) noexcept:
|
||||||
|
cdef int color = self._card_color(card)
|
||||||
|
cdef int rank = self._card_rank(card)
|
||||||
|
if color < 0 or color >= self.n_colors:
|
||||||
|
return False
|
||||||
|
if rank < 0 or rank > self.n_ranks:
|
||||||
|
return False
|
||||||
|
if rank == 0:
|
||||||
|
return self.last_numeric_ranks[self._expedition_len_index(player, color)] == 0
|
||||||
|
return rank > self.last_numeric_ranks[self._expedition_len_index(player, color)]
|
||||||
|
|
||||||
|
cdef void _fill_undo_c(self, int action_id, UndoRecord* undo) noexcept:
|
||||||
|
cdef int slot
|
||||||
|
cdef int card
|
||||||
|
cdef int color
|
||||||
|
cdef int cache_index
|
||||||
|
undo.phase_id = self.phase_id
|
||||||
|
undo.player = self.current_player
|
||||||
|
undo.action_id = action_id
|
||||||
|
undo.pending_before = self.pending_discarded_color
|
||||||
|
undo.terminal_before = self.terminal
|
||||||
|
undo.turn_count_before = self.turn_count
|
||||||
|
undo.slot = -1
|
||||||
|
undo.play = 0
|
||||||
|
undo.card = -1
|
||||||
|
undo.color = -1
|
||||||
|
undo.last_numeric_before = 0
|
||||||
|
undo.handshake_count_before = 0
|
||||||
|
undo.numeric_sum_before = 0
|
||||||
|
undo.expedition_score_before = 0
|
||||||
|
undo.total_score_before = self.total_scores[self.current_player]
|
||||||
|
if self.phase_id == _phase_card():
|
||||||
|
slot = action_id // 2
|
||||||
|
card = self.hands[self._hand_index(self.current_player, slot)]
|
||||||
|
color = self._card_color(card)
|
||||||
|
cache_index = self._expedition_len_index(self.current_player, color)
|
||||||
|
undo.slot = slot
|
||||||
|
undo.play = action_id % 2 == 0
|
||||||
|
undo.card = card
|
||||||
|
undo.color = color
|
||||||
|
undo.last_numeric_before = self.last_numeric_ranks[cache_index]
|
||||||
|
undo.handshake_count_before = self.handshake_counts[cache_index]
|
||||||
|
undo.numeric_sum_before = self.numeric_sums[cache_index]
|
||||||
|
undo.expedition_score_before = self.expedition_scores[cache_index]
|
||||||
|
elif action_id == 0:
|
||||||
|
undo.card = self.deck[self.deck_len - 1]
|
||||||
else:
|
else:
|
||||||
card = self.discards[self._discard_index(action_id - 1, self.discard_lens[action_id - 1] - 1)]
|
color = action_id - 1
|
||||||
|
undo.color = color
|
||||||
|
undo.card = self.discards[self._discard_index(color, self.discard_lens[color] - 1)]
|
||||||
|
|
||||||
|
cdef void _apply_action_with_undo_c(self, int action_id, UndoRecord* undo) except *:
|
||||||
|
self._fill_undo_c(action_id, undo)
|
||||||
|
self._apply_action_unchecked_c(action_id)
|
||||||
|
|
||||||
|
cdef void _apply_action_unchecked_c(self, int action_id) except *:
|
||||||
|
if self.phase_id == _phase_card():
|
||||||
|
self._apply_card_action(action_id)
|
||||||
|
else:
|
||||||
|
self._apply_draw_action(action_id)
|
||||||
|
|
||||||
|
cdef object _undo_to_tuple(self, UndoRecord* undo):
|
||||||
return (
|
return (
|
||||||
"draw",
|
"card" if undo.phase_id == _phase_card() else "draw",
|
||||||
self.current_player,
|
undo.player,
|
||||||
action_id,
|
undo.action_id,
|
||||||
self.pending_discarded_color,
|
undo.pending_before,
|
||||||
self.terminal,
|
undo.terminal_before,
|
||||||
self.turn_count,
|
undo.turn_count_before,
|
||||||
card,
|
undo.slot,
|
||||||
|
undo.play,
|
||||||
|
undo.card,
|
||||||
|
undo.color,
|
||||||
|
undo.last_numeric_before,
|
||||||
|
undo.handshake_count_before,
|
||||||
|
undo.numeric_sum_before,
|
||||||
|
undo.expedition_score_before,
|
||||||
|
undo.total_score_before,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cdef void _tuple_to_undo(self, object data, UndoRecord* undo) except *:
|
||||||
|
cdef str phase = data[0]
|
||||||
|
if phase == "card":
|
||||||
|
undo.phase_id = _phase_card()
|
||||||
|
elif phase == "draw":
|
||||||
|
undo.phase_id = _phase_draw()
|
||||||
|
else:
|
||||||
|
raise ValueError(f"invalid undo phase: {phase!r}")
|
||||||
|
undo.player = <int>data[1]
|
||||||
|
undo.action_id = <int>data[2]
|
||||||
|
undo.pending_before = <int>data[3]
|
||||||
|
undo.terminal_before = <bint>data[4]
|
||||||
|
undo.turn_count_before = <int>data[5]
|
||||||
|
undo.slot = <int>data[6]
|
||||||
|
undo.play = <int>data[7]
|
||||||
|
undo.card = <int>data[8]
|
||||||
|
undo.color = <int>data[9]
|
||||||
|
undo.last_numeric_before = <int>data[10]
|
||||||
|
undo.handshake_count_before = <int>data[11]
|
||||||
|
undo.numeric_sum_before = <int>data[12]
|
||||||
|
undo.expedition_score_before = <int>data[13]
|
||||||
|
undo.total_score_before = <int>data[14]
|
||||||
|
|
||||||
cdef void _apply_card_action(self, int action_id) except *:
|
cdef void _apply_card_action(self, int action_id) except *:
|
||||||
cdef int slot = action_id // 2
|
cdef int slot = action_id // 2
|
||||||
cdef bint play = action_id % 2 == 0
|
cdef bint play = action_id % 2 == 0
|
||||||
cdef int player = self.current_player
|
cdef int player = self.current_player
|
||||||
cdef int card = self.hands[self._hand_index(player, slot)]
|
cdef int card = self.hands[self._hand_index(player, slot)]
|
||||||
cdef int color = self._card_color(card)
|
cdef int color = self._card_color(card)
|
||||||
|
cdef int rank = self._card_rank(card)
|
||||||
cdef int i
|
cdef int i
|
||||||
cdef int length_index
|
cdef int length_index
|
||||||
|
cdef int old_score
|
||||||
|
cdef int new_score
|
||||||
for i in range(slot, self.hand_lens[player] - 1):
|
for i in range(slot, self.hand_lens[player] - 1):
|
||||||
self.hands[self._hand_index(player, i)] = self.hands[self._hand_index(player, i + 1)]
|
self.hands[self._hand_index(player, i)] = self.hands[self._hand_index(player, i + 1)]
|
||||||
self.hand_lens[player] -= 1
|
self.hand_lens[player] -= 1
|
||||||
if play:
|
if play:
|
||||||
length_index = self._expedition_len_index(player, color)
|
length_index = self._expedition_len_index(player, color)
|
||||||
|
old_score = self.expedition_scores[length_index]
|
||||||
self.expeditions[self._expedition_index(player, color, self.expedition_lens[length_index])] = card
|
self.expeditions[self._expedition_index(player, color, self.expedition_lens[length_index])] = card
|
||||||
self.expedition_lens[length_index] += 1
|
self.expedition_lens[length_index] += 1
|
||||||
|
if rank == 0:
|
||||||
|
self.handshake_counts[length_index] += 1
|
||||||
|
else:
|
||||||
|
self.numeric_sums[length_index] += self.min_rank + rank - 1
|
||||||
|
self.last_numeric_ranks[length_index] = rank
|
||||||
|
new_score = self._score_from_summary_c(
|
||||||
|
self.expedition_lens[length_index],
|
||||||
|
self.handshake_counts[length_index],
|
||||||
|
self.numeric_sums[length_index],
|
||||||
|
)
|
||||||
|
self.expedition_scores[length_index] = new_score
|
||||||
|
self.total_scores[player] += new_score - old_score
|
||||||
else:
|
else:
|
||||||
self.discards[self._discard_index(color, self.discard_lens[color])] = card
|
self.discards[self._discard_index(color, self.discard_lens[color])] = card
|
||||||
self.discard_lens[color] += 1
|
self.discard_lens[color] += 1
|
||||||
@@ -567,13 +773,21 @@ cdef class FastGameState:
|
|||||||
self.current_player = 1 - self.current_player
|
self.current_player = 1 - self.current_player
|
||||||
self.phase_id = _phase_card()
|
self.phase_id = _phase_card()
|
||||||
|
|
||||||
cdef void _undo_card_action(self, object undo) except *:
|
cdef void _undo_action_c(self, UndoRecord* undo) except *:
|
||||||
cdef int player = <int>undo[1]
|
if undo.phase_id == _phase_card():
|
||||||
cdef int pending_before = <int>undo[3]
|
self._undo_card_action_c(undo)
|
||||||
cdef bint terminal_before = <bint>undo[4]
|
elif undo.phase_id == _phase_draw():
|
||||||
cdef int slot = <int>undo[5]
|
self._undo_draw_action_c(undo)
|
||||||
cdef bint play = <bint>undo[6]
|
else:
|
||||||
cdef int card = <int>undo[7]
|
raise ValueError("invalid undo phase")
|
||||||
|
|
||||||
|
cdef void _undo_card_action_c(self, UndoRecord* undo) except *:
|
||||||
|
cdef int player = undo.player
|
||||||
|
cdef int pending_before = undo.pending_before
|
||||||
|
cdef bint terminal_before = undo.terminal_before
|
||||||
|
cdef int slot = undo.slot
|
||||||
|
cdef bint play = undo.play
|
||||||
|
cdef int card = undo.card
|
||||||
cdef int color = self._card_color(card)
|
cdef int color = self._card_color(card)
|
||||||
cdef int moved
|
cdef int moved
|
||||||
cdef int i
|
cdef int i
|
||||||
@@ -582,6 +796,11 @@ cdef class FastGameState:
|
|||||||
length_index = self._expedition_len_index(player, color)
|
length_index = self._expedition_len_index(player, color)
|
||||||
self.expedition_lens[length_index] -= 1
|
self.expedition_lens[length_index] -= 1
|
||||||
moved = self.expeditions[self._expedition_index(player, color, self.expedition_lens[length_index])]
|
moved = self.expeditions[self._expedition_index(player, color, self.expedition_lens[length_index])]
|
||||||
|
self.last_numeric_ranks[length_index] = undo.last_numeric_before
|
||||||
|
self.handshake_counts[length_index] = undo.handshake_count_before
|
||||||
|
self.numeric_sums[length_index] = undo.numeric_sum_before
|
||||||
|
self.expedition_scores[length_index] = undo.expedition_score_before
|
||||||
|
self.total_scores[player] = undo.total_score_before
|
||||||
else:
|
else:
|
||||||
self.discard_lens[color] -= 1
|
self.discard_lens[color] -= 1
|
||||||
moved = self.discards[self._discard_index(color, self.discard_lens[color])]
|
moved = self.discards[self._discard_index(color, self.discard_lens[color])]
|
||||||
@@ -596,13 +815,13 @@ cdef class FastGameState:
|
|||||||
self.pending_discarded_color = pending_before
|
self.pending_discarded_color = pending_before
|
||||||
self.terminal = terminal_before
|
self.terminal = terminal_before
|
||||||
|
|
||||||
cdef void _undo_draw_action(self, object undo) except *:
|
cdef void _undo_draw_action_c(self, UndoRecord* undo) except *:
|
||||||
cdef int player = <int>undo[1]
|
cdef int player = undo.player
|
||||||
cdef int action_id = <int>undo[2]
|
cdef int action_id = undo.action_id
|
||||||
cdef int pending_before = <int>undo[3]
|
cdef int pending_before = undo.pending_before
|
||||||
cdef bint terminal_before = <bint>undo[4]
|
cdef bint terminal_before = undo.terminal_before
|
||||||
cdef int turn_count_before = <int>undo[5]
|
cdef int turn_count_before = undo.turn_count_before
|
||||||
cdef int card = <int>undo[6]
|
cdef int card = undo.card
|
||||||
cdef int moved
|
cdef int moved
|
||||||
cdef int color
|
cdef int color
|
||||||
self.hand_lens[player] -= 1
|
self.hand_lens[player] -= 1
|
||||||
@@ -622,6 +841,58 @@ cdef class FastGameState:
|
|||||||
self.turn_count = turn_count_before
|
self.turn_count = turn_count_before
|
||||||
self.terminal = terminal_before
|
self.terminal = terminal_before
|
||||||
|
|
||||||
|
cdef void _recompute_score_caches(self) noexcept:
|
||||||
|
cdef int i
|
||||||
|
cdef int player
|
||||||
|
cdef int color
|
||||||
|
cdef int cache_index
|
||||||
|
cdef int length
|
||||||
|
cdef int rank
|
||||||
|
cdef int card_index
|
||||||
|
for i in range(2 * self.n_colors):
|
||||||
|
self.last_numeric_ranks[i] = 0
|
||||||
|
self.handshake_counts[i] = 0
|
||||||
|
self.numeric_sums[i] = 0
|
||||||
|
self.expedition_scores[i] = 0
|
||||||
|
self.total_scores[0] = 0
|
||||||
|
self.total_scores[1] = 0
|
||||||
|
for player in range(2):
|
||||||
|
for color in range(self.n_colors):
|
||||||
|
cache_index = self._expedition_len_index(player, color)
|
||||||
|
length = self.expedition_lens[cache_index]
|
||||||
|
for card_index in range(length):
|
||||||
|
rank = self._card_rank(
|
||||||
|
self.expeditions[
|
||||||
|
self._expedition_index(player, color, card_index)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if rank == 0:
|
||||||
|
self.handshake_counts[cache_index] += 1
|
||||||
|
else:
|
||||||
|
self.numeric_sums[cache_index] += self.min_rank + rank - 1
|
||||||
|
if rank > self.last_numeric_ranks[cache_index]:
|
||||||
|
self.last_numeric_ranks[cache_index] = rank
|
||||||
|
self.expedition_scores[cache_index] = self._score_from_summary_c(
|
||||||
|
length,
|
||||||
|
self.handshake_counts[cache_index],
|
||||||
|
self.numeric_sums[cache_index],
|
||||||
|
)
|
||||||
|
self.total_scores[player] += self.expedition_scores[cache_index]
|
||||||
|
|
||||||
|
cdef inline int _score_from_summary_c(
|
||||||
|
self,
|
||||||
|
int length,
|
||||||
|
int handshakes,
|
||||||
|
int numeric_sum,
|
||||||
|
) noexcept:
|
||||||
|
cdef int score
|
||||||
|
if length == 0:
|
||||||
|
return 0
|
||||||
|
score = (numeric_sum + self.expedition_penalty) * (handshakes + 1)
|
||||||
|
if length >= self.bonus_threshold:
|
||||||
|
score += self.bonus_amount
|
||||||
|
return score
|
||||||
|
|
||||||
cdef bint _has_any_legal_draw(self):
|
cdef bint _has_any_legal_draw(self):
|
||||||
cdef int color
|
cdef int color
|
||||||
if self.deck_len > 0:
|
if self.deck_len > 0:
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ def test_fast_random_action_sequence_matches_game_state() -> None:
|
|||||||
while True:
|
while True:
|
||||||
assert fast.to_snapshot() == classic.to_snapshot()
|
assert fast.to_snapshot() == classic.to_snapshot()
|
||||||
assert fast.unified_legal_mask() == classic.unified_legal_mask()
|
assert fast.unified_legal_mask() == classic.unified_legal_mask()
|
||||||
|
assert fast.unified_legal_actions() == [
|
||||||
|
index for index, is_legal in enumerate(classic.unified_legal_mask()) if is_legal
|
||||||
|
]
|
||||||
assert fast.score_diff(0) == classic.score_diff(0)
|
assert fast.score_diff(0) == classic.score_diff(0)
|
||||||
if classic.terminal:
|
if classic.terminal:
|
||||||
break
|
break
|
||||||
|
|||||||
Reference in New Issue
Block a user