Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,9 @@ non-decreasing country rank, most important first or last, one sequence per boar
points releases its floor (`engine/AGENTS.md` §21); a branch adds one with `--resume-add-rules`, and a person
plays neither. Two observation bits of no width let a network see its run's order: `PLACEMENT_LEGAL` (bit 7,
the mask in the "I can place here" slot, made V1 worse) and `PLACEMENT_ORDER` (bit 8, that slot keeps access and
marks only order-blocked countries 0.5). A seat plays the rule bits of the feature set it is observed in, a
marks only order-blocked countries 0.5). `OWN_EVENT_GATED` (bit 9) and `ALWAYS_ASK_DISCARD` (bit 10, 2026-11-11) are
rule bits a person plays and no network does (no training name): no Event offered that would do nothing, and Blockade
/ Latin American Debt Crisis always ask the US (`engine/AGENTS.md` §22). A seat plays the rule bits of the feature set it is observed in, a
search both sides of its tree under its own network's, and a person in the web page every rule bit; an
unseated side plays the rule from before every bit, so the unseated engine is E8's.
Adding a block still needs the owner.
Expand Down
4 changes: 3 additions & 1 deletion ai/models/ladder_net.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@
1 << 5: 0, # CANONICAL_LAST (P43, a rule bit, no width)
1 << 6: 0, # CANONICAL_LEARNED (P43 V3, a rule bit, no width)
1 << 7: 0, # PLACEMENT_LEGAL (edits base slot 19, no width)
1 << 8: 0} # PLACEMENT_ORDER (edits base slot 19, no width)
1 << 8: 0, # PLACEMENT_ORDER (edits base slot 19, no width)
1 << 9: 0, # OWN_EVENT_GATED (a person's rule bit, no width)
1 << 10: 0} # ALWAYS_ASK_DISCARD (a person's rule bit, no width)


def _obs_extra(features: int) -> int:
Expand Down
13 changes: 12 additions & 1 deletion bindings/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,18 @@ are cited from elsewhere; keep them.
`ts_env.seat_tree`; `seat_decider` seats the real position's decider. `(seed, actions)` datasets
record `us_obs_features` / `ussr_obs_features`, which `WarmupDataset` seats (absent: 0). The wasm
build exports `ts_seat_obs_features`, `ts_seat_rules` and `ts_obs_features_rules`, the rules a
person plays: `OBS_FEATURES_PERSON_RULES`, every rule bit but the canonical orders.
person plays: `OBS_FEATURES_PERSON_RULES`, every rule bit but the canonical orders
(`OBS_FEATURE_OPP_EVENT_GATED`, and `OBS_FEATURE_OWN_EVENT_GATED` / `OBS_FEATURE_ALWAYS_ASK_DISCARD`,
a person's rules with no training name, [`engine/AGENTS.md`](../engine/AGENTS.md) §22).
- **For pages that mirror a game played elsewhere** (Twilight Struggle /ai, the analysis board): the wasm
build also exports `ts_step_dice` (a click with both dice of an opposed roll forced), `ts_first_headline_json`
(a copy of the game stopped between the two headlines), `ts_card_location` / `ts_set_card_location` and
`ts_set_rng` (put a real deal or draw on the board), and `ts_rollout_leaf_features` /
`ts_rollout_leaf_feature_count` (Europe at each rollout leaf). `ts_step` accepts UN Intervention played on the
card Grain Sales drew, the one action `StateMachine::step` takes for another decision type. The display
state says, per card in hand, whether its Event would happen now (`event_ready`), who owes Missile Envy's Ops
(`MISSILE_ENVY_US` / `_USSR` in `flags`), Chernobyl's region (`chernobyl_region`), whether "done" is legal
(`legal_actions.can_decline`) and, at Grain Sales' card, whether UN Intervention is (`legal_actions.un_intervention`).
- **`BatchedSearch`** ([`batched_search.hpp`](batched_search.hpp)): the C++ tree behind
`ai/search/batched_mcts.py` (`backend="cpp"`), Python-only. Up to `capacity` PUCT searches in
lockstep, split into `num_groups` evaluation groups: `reset(roots, simulations, rng_state,
Expand Down
2 changes: 2 additions & 0 deletions bindings/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ if(EMSCRIPTEN)
_ts_belief_temperature_weights _ts_belief_worlds _ts_rollout_add_belief
_ts_rollout_race _ts_rollout_in_race _ts_obs_size_for _ts_observation_features _ts_click_event_tracks_json
_ts_seat_obs_features _ts_seat_rules _ts_obs_features_rules
_ts_step_dice _ts_first_headline_json _ts_card_location _ts_set_card_location _ts_set_rng
_ts_rollout_leaf_features _ts_rollout_leaf_feature_count
_malloc _free)
list(JOIN TS_WASM_EXPORTS "," TS_WASM_EXPORTS_CSV)
target_link_options(ts_engine_wasm PRIVATE
Expand Down
1 change: 1 addition & 0 deletions bindings/rollout_batch.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ class RolloutBatch {
bool all_done() const noexcept { return live() == 0; }
size_t listed() const noexcept { return live_order_.size(); } // rows the last fill_live wrote
size_t leaves_listed() const noexcept { return leaf_order_.size(); } // rows the last fill_leaves wrote
uint32_t leaf_row(size_t j) const noexcept { return leaf_order_[j]; } // the row of the j-th leaf listed
// Rows at their horizon still waiting for a leaf value: what the next fill_leaves writes.
size_t leaves_pending() const noexcept {
size_t n = 0;
Expand Down
35 changes: 35 additions & 0 deletions bindings/state_json.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
#include <limits>
#include <type_traits>

#include "ts/action_mask.hpp"
#include "ts/card_data.hpp"
#include "ts/card_handlers.hpp"
#include "ts/engine.hpp"
#include "ts/map_data.hpp"

Expand Down Expand Up @@ -583,9 +585,20 @@ Value display_state(const GameState& state) {
CHECK_FLAG(CMC_SUICIDE_LOSS);
CHECK_FLAG(EUROPE_CONTROL_WIN);
#undef CHECK_FLAG
// Not an effect bit: who received Missile Envy and still holds it, owing its Operations.
if (state.forced_card_id == card_ids::MISSILE_ENVY && state.forced_card_player != Player::NONE &&
in_hand_of(state.card_locations[card_ids::MISSILE_ENVY], state.forced_card_player)) {
flags.push(state.forced_card_player == Player::US ? "MISSILE_ENVY_US" : "MISSILE_ENVY_USSR");
}
d.set("flags", std::move(flags));
// uint64: exact in Python; a JS reader must use `flags`, not this, past 2^53.
d.set("persistent_effects", Value(static_cast<uint64_t>(state.persistent_effects)));
// Chernobyl's region (engine `Region` order), while it is in effect: its bits are past what a
// JS number keeps exactly once higher flags are set.
if (state.has_flag(effect_bits::CHERNOBYL_ACTIVE)) {
d.set("chernobyl_region", I(static_cast<int>((state.persistent_effects & effect_bits::CHERNOBYL_REGION_MASK)
>> effect_bits::CHERNOBYL_REGION_SHIFT)));
}

// 6. Countries, by name
Value countries = Value::object();
Expand Down Expand Up @@ -625,6 +638,15 @@ Value display_state(const GameState& state) {
ci.set("id", I(i));
ci.set("name", std::string(CardData::get_card_name(i)));
ci.set("ops", I(CardData::get_card(i).ops));
// Whether its Event would happen were it played now: for its holder, or for the
// opponent whose card it is (an opponent's card fires for them when played).
{
const Player holder = us ? Player::US : Player::USSR;
const Player side = CardData::is_opponent_card(i, holder) ? get_opponent(holder) : holder;
ci.set("event_ready", i != card_ids::THE_CHINA_CARD &&
CardHandlers::can_trigger_event(state, i, side) &&
CardHandlers::event_has_effect(state, i, side));
}
(us ? us_hand : ussr_hand).push(I(i));
(us ? us_cards : ussr_cards).push(std::move(ci));
break;
Expand Down Expand Up @@ -705,6 +727,19 @@ Value display_state(const GameState& state) {
legal_actions.set("valid_ids", std::move(legal));
legal_actions.set("valid_action_labels", std::move(labels));
legal_actions.set("allow_early_stop", Value(ctx.allow_early_stop != 0));
// Whether the engine takes "done" here (the flat mask's CONFIRM_DONE): an early stop, a
// choice with nothing left to choose, or an optional bonus the player may decline (Junta's
// and Tear Down This Wall's free coup or realignment) -- which `allow_early_stop` misses.
{
uint8_t flat[FLAT_ACTION_SPACE_SIZE];
ActionMask::generate_flat_mask_212(state, flat);
legal_actions.set("can_decline", Value(flat[flat_slots::CONFIRM_DONE] != 0));
// UN Intervention played on the card Grain Sales drew: a card slot at a resolution node,
// which `valid_ids` (the play modes) cannot say.
if (ctx.decision_type == DecisionType::SELECT_PLAY_MODE && ctx.resolving_card == card_ids::GRAIN_SALES) {
legal_actions.set("un_intervention", Value(flat[card_ids::UN_INTERVENTION - 1] != 0));
}
}
d.set("legal_actions", std::move(legal_actions));

const bool terminal = Engine::is_terminal(state);
Expand Down
2 changes: 2 additions & 0 deletions bindings/ts_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1506,6 +1506,8 @@ NB_MODULE(ts_engine, m) {
m.attr("OBS_FEATURE_CANONICAL_LEARNED") = static_cast<uint32_t>(ts::obs_features::CANONICAL_LEARNED);
m.attr("OBS_FEATURE_PLACEMENT_LEGAL") = static_cast<uint32_t>(ts::obs_features::PLACEMENT_LEGAL);
m.attr("OBS_FEATURE_PLACEMENT_ORDER") = static_cast<uint32_t>(ts::obs_features::PLACEMENT_ORDER);
m.attr("OBS_FEATURE_OWN_EVENT_GATED") = static_cast<uint32_t>(ts::obs_features::OWN_EVENT_GATED);
m.attr("OBS_FEATURE_ALWAYS_ASK_DISCARD") = static_cast<uint32_t>(ts::obs_features::ALWAYS_ASK_DISCARD);
m.attr("OBS_FEATURES_RULES") = static_cast<uint32_t>(ts::obs_features::RULES);
m.attr("OBS_FEATURES_PERSON_RULES") = static_cast<uint32_t>(ts::obs_features::PERSON_RULES);
m.attr("OBS_FEATURES_ALL") = static_cast<uint32_t>(ts::obs_features::ALL);
Expand Down
10 changes: 7 additions & 3 deletions bindings/ts_engine/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1183,11 +1183,15 @@ OBS_FEATURE_PLACEMENT_LEGAL: int = 128

OBS_FEATURE_PLACEMENT_ORDER: int = 256

OBS_FEATURES_RULES: int = 120
OBS_FEATURE_OWN_EVENT_GATED: int = 512

OBS_FEATURES_PERSON_RULES: int = 8
OBS_FEATURE_ALWAYS_ASK_DISCARD: int = 1024

OBS_FEATURES_ALL: int = 510
OBS_FEATURES_RULES: int = 1656

OBS_FEATURES_PERSON_RULES: int = 1544

OBS_FEATURES_ALL: int = 2046

OBS_FLAG_STAGED_CARDS: int = 1

Expand Down
Loading