Rust 크레이트를 루트로 이동
맥락: - GUI 이식 전에 Rust crate와 proto schema 위치를 Python package 내부에서 분리한다. - Cargo 작업, IDE 인식, 빌드 산출물 관리를 루트 구조에 맞춘다. 변경: - rust_core를 rust/lost-cities-core로 이동하고 proto/lost_cities.proto를 루트 proto 디렉터리로 옮겼다. - Rust build.rs, Python Rust backend, Rust parity 테스트의 경로를 새 위치로 수정했다. - Python package-data에서 Rust crate와 proto 항목을 제거하고 README/port notes를 갱신했다. 확인: - uv sync --extra gui --reinstall-package coolrl-lost-cities - uv run pytest tests/games/classic - uv run lost-cities-classic
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
syntax = "proto3";
|
||||
package lost_cities.v1;
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
|
||||
// See lost_cities_spec.md for the full game rules.
|
||||
// This proto is the wire contract for external agents to play Lost Cities
|
||||
// against a trusted orchestrator. RL training uses in-process PyO3 bindings
|
||||
// instead and does not go through this service.
|
||||
|
||||
// =============================================================
|
||||
// Core value types
|
||||
// =============================================================
|
||||
|
||||
// A single card. Rank 0 is a handshake (investment) card.
|
||||
// Rank 1..n_ranks are numeric cards whose printed face value is
|
||||
// min_rank + rank - 1.
|
||||
message Card {
|
||||
uint32 color = 1;
|
||||
uint32 rank = 2;
|
||||
uint32 numeric_value = 3; // 0 for handshake, else min_rank + rank - 1
|
||||
string label = 4; // debug-friendly e.g. "[2]H", "[0]5"
|
||||
// non-authoritative, do not parse
|
||||
}
|
||||
|
||||
enum Phase {
|
||||
PHASE_UNSPECIFIED = 0;
|
||||
PHASE_CARD = 1; // the current player must play or discard a card
|
||||
PHASE_DRAW = 2; // the current player must draw from deck or a discard pile
|
||||
}
|
||||
|
||||
enum ActionKind {
|
||||
ACTION_KIND_UNSPECIFIED = 0;
|
||||
ACTION_KIND_PLAY_CARD = 1;
|
||||
ACTION_KIND_DISCARD_CARD = 2;
|
||||
ACTION_KIND_DRAW_DECK = 3;
|
||||
ACTION_KIND_DRAW_DISCARD = 4;
|
||||
}
|
||||
|
||||
// A resolved legal action.
|
||||
//
|
||||
// `id` is an opaque integer handle valid only within the observation it
|
||||
// was returned with. Clients pass it back via ApplyAction together with
|
||||
// the observation's state_version.
|
||||
//
|
||||
// `kind` plus typed payload fields let semantic agents (LLM, rule-based)
|
||||
// reason without decoding ids. `description` is a human-readable label
|
||||
// such as "Play yellow 5" and must not be parsed by the agent.
|
||||
message Action {
|
||||
uint32 id = 1;
|
||||
ActionKind kind = 2;
|
||||
|
||||
// Set for PLAY_CARD and DISCARD_CARD.
|
||||
uint32 hand_slot = 3;
|
||||
Card card = 4;
|
||||
|
||||
// Set for DRAW_DISCARD.
|
||||
uint32 discard_color = 5;
|
||||
|
||||
string description = 6;
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// Configuration
|
||||
// =============================================================
|
||||
|
||||
message GameConfig {
|
||||
uint32 n_colors = 1;
|
||||
uint32 n_ranks = 2;
|
||||
uint32 min_rank = 3;
|
||||
uint32 n_handshakes = 4;
|
||||
uint32 hand_size = 5;
|
||||
int32 expedition_penalty = 6; // typically negative, e.g. -20
|
||||
uint32 bonus_threshold = 7;
|
||||
int32 bonus_amount = 8;
|
||||
optional uint64 seed = 9; // absent = nondeterministic shuffle
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// Piles and expeditions
|
||||
// =============================================================
|
||||
|
||||
message Expedition {
|
||||
uint32 color = 1;
|
||||
repeated Card cards = 2; // bottom to top, push-only during game
|
||||
int32 current_score = 3; // derived from the game rules
|
||||
}
|
||||
|
||||
message DiscardPile {
|
||||
uint32 color = 1;
|
||||
repeated Card cards = 2; // bottom to top; last element is drawable top
|
||||
uint32 size = 3; // = cards.size(); convenience
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// Legal actions
|
||||
// =============================================================
|
||||
|
||||
// The set of legal actions at a specific state.
|
||||
//
|
||||
// Invariants:
|
||||
// - actions are sorted by id ascending
|
||||
// - for every action a in actions: mask[a.id] == true
|
||||
// - mask.size() == action_space_size
|
||||
// - all ids are < action_space_size
|
||||
// - state_version matches the observation this set was produced for
|
||||
message LegalActionSet {
|
||||
uint64 state_version = 1;
|
||||
repeated Action actions = 2;
|
||||
repeated bool mask = 3;
|
||||
uint32 action_space_size = 4;
|
||||
Phase phase = 5;
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// Observation
|
||||
// =============================================================
|
||||
|
||||
// Player-perspective snapshot. `observer_player` identifies whose view
|
||||
// this is. The observer sees their own hand; the opponent's hand is
|
||||
// represented only by its size.
|
||||
message GameObservation {
|
||||
string session_id = 1;
|
||||
GameConfig config = 2;
|
||||
|
||||
// Monotonically increasing per session. Required for ApplyAction.
|
||||
uint64 state_version = 3;
|
||||
|
||||
uint32 observer_player = 4;
|
||||
uint32 current_player = 5;
|
||||
Phase phase = 6;
|
||||
|
||||
repeated Card hand = 7; // observer's hand, sorted
|
||||
uint32 opponent_hand_size = 8;
|
||||
|
||||
repeated Expedition my_expeditions = 9;
|
||||
repeated Expedition opponent_expeditions = 10;
|
||||
repeated DiscardPile discards = 11;
|
||||
|
||||
uint32 deck_size = 12;
|
||||
|
||||
// Color just discarded during the current turn's card phase.
|
||||
// The current player's draw phase may not re-draw that color
|
||||
// from the discard pile. Cleared at turn boundary.
|
||||
optional uint32 pending_discarded_color = 13;
|
||||
|
||||
// Legal actions for the player to act. Empty when terminal.
|
||||
// Embedded to avoid a round-trip on the common observe->select->apply loop.
|
||||
LegalActionSet legal_actions = 14;
|
||||
|
||||
uint32 turn_count = 15;
|
||||
bool terminal = 16;
|
||||
|
||||
// Scores are from the observer's perspective.
|
||||
int32 my_score = 17;
|
||||
int32 opponent_score = 18;
|
||||
int32 score_diff = 19; // my_score - opponent_score
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// Requests and responses
|
||||
// =============================================================
|
||||
|
||||
message NewGameRequest {
|
||||
string session_id = 1;
|
||||
GameConfig config = 2;
|
||||
}
|
||||
|
||||
message SessionRef {
|
||||
string session_id = 1;
|
||||
// Defaults to current_player when absent.
|
||||
optional uint32 observer_player = 2;
|
||||
}
|
||||
|
||||
message ApplyActionRequest {
|
||||
string session_id = 1;
|
||||
uint32 action_id = 2;
|
||||
|
||||
// Must match the observation the action was selected from.
|
||||
// Mismatch yields FAILED_PRECONDITION; no state change occurs.
|
||||
// This makes ApplyAction safely retryable.
|
||||
uint64 expected_state_version = 3;
|
||||
|
||||
// Perspective for the returned observation. Defaults to current_player
|
||||
// after the action is applied.
|
||||
optional uint32 observer_player = 4;
|
||||
}
|
||||
|
||||
message StepResult {
|
||||
GameObservation observation = 1;
|
||||
|
||||
// From the observation's observer_player perspective.
|
||||
// Sparse: nonzero only on the terminal transition, equal to score_diff.
|
||||
double reward = 2;
|
||||
|
||||
bool terminal = 3;
|
||||
|
||||
// Populated on terminal transitions. Keyed by player index.
|
||||
map<uint32, int32> final_scores = 4;
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// Service
|
||||
// =============================================================
|
||||
|
||||
service LostCities {
|
||||
// Start a new game. session_id must be unique; duplicates return
|
||||
// ALREADY_EXISTS. The returned observation is from player 0's perspective.
|
||||
rpc NewGame(NewGameRequest) returns (GameObservation);
|
||||
|
||||
// Re-read the current state without advancing. Useful after reconnects
|
||||
// or for a second client joining an existing session.
|
||||
rpc GetObservation(SessionRef) returns (GameObservation);
|
||||
|
||||
// Apply one action. Fails with:
|
||||
// NOT_FOUND session_id unknown
|
||||
// FAILED_PRECONDITION expected_state_version mismatch,
|
||||
// action_id not legal in current state,
|
||||
// or game already terminal
|
||||
// INVALID_ARGUMENT malformed request
|
||||
rpc ApplyAction(ApplyActionRequest) returns (StepResult);
|
||||
|
||||
// Release server-side session state. Idempotent.
|
||||
rpc EndSession(SessionRef) returns (google.protobuf.Empty);
|
||||
}
|
||||
Reference in New Issue
Block a user