Serve Python game session surfaces

This commit is contained in:
Meta Vens 2026-06-14 17:11:43 +01:00
commit fdc80db44e
3 changed files with 108 additions and 1 deletions

View file

@ -202,7 +202,10 @@ CultCache, CultNet, and CultMesh. It is evidence, not confetti.
- Python CultMesh session watches:
`CultMeshGameSession.watch_candidates(...)` and
`watch_simulation_facts(...)` expose the same gameplay-facing observation and
committed-fact watch points as the C# session facade.
committed-fact watch points as the C# session facade. A game session can also
serve its own Verse catalog, peer catalog, and simulation observation hub over
the local framed CultMesh server so peer-facing session state uses the same
wire contracts as lower-level server callers.
- Python package health:
`cultcache-py-verify --json` checks the public CultCache/CultNet/CultMesh
export surface declared by each package `__all__`, `py.typed` markers, a live

View file

@ -51,6 +51,9 @@ class CultMeshGameSessionOptions:
authority_leases: CultMeshAuthorityLeaseCatalog | None = None
consensus_options: CultNetSimulationConsensusOptions | None = None
client_authority_scopes: tuple[CultNetClientAuthorityScope, ...] = ()
serve_verse_discovery: bool = True
serve_peer_exchange: bool = True
serve_simulation_observations: bool = True
@dataclass
@ -70,6 +73,32 @@ class CultMeshGameSession:
self._committed_fact_ids: set[str] = set()
self._client_authority_scopes = tuple(options.client_authority_scopes)
self._predictions: dict[tuple[str, str], CultMeshPrediction] = {}
self._serve_verse_discovery = options.serve_verse_discovery
self._serve_peer_exchange = options.serve_peer_exchange
self._serve_simulation_observations = options.serve_simulation_observations
def serve(
self,
*,
host: str = "127.0.0.1",
port: int = 0,
display_name: str | None = None,
max_snapshot_documents: int | None = None,
max_snapshot_bytes: int | None = None,
) -> Any:
from .server import CultMeshLocalServer
return CultMeshLocalServer(
node=self.node,
verse_catalog=self.verse_catalog if self._serve_verse_discovery else CultMeshVerseCatalog(),
peer_catalog=self.peer_catalog if self._serve_peer_exchange else CultMeshPeerCatalog(),
observation_hub=self.observation_hub if self._serve_simulation_observations else None,
host=host,
port=port,
display_name=display_name,
max_snapshot_documents=max_snapshot_documents,
max_snapshot_bytes=max_snapshot_bytes,
).start()
def predict(self, document: DocumentDefinition[Any], key: str, value: Any) -> CultMeshPrediction:
schema_id = document.catalog_entry().schema_id

View file

@ -2132,6 +2132,81 @@ class CultCacheTests(unittest.TestCase):
self.assertEqual(stored.claim_hash, claim_hash)
self.assertEqual(stored.witness_count, 2)
def test_cultmesh_game_session_serves_catalogs_and_observations_over_wire(self) -> None:
verse_catalog = CultMeshVerseCatalog()
peer_catalog = CultMeshPeerCatalog()
verse_catalog.upsert(CultMeshVerseDescriptor(
verse_id="session-verse",
display_name="Session Verse",
authority_model="local",
compatibility=CultMeshVerseCompatibility("cultmesh.v0", "session-rules"),
))
peer_catalog.upsert(CultMeshPeerCard(
peer_id="session-peer",
verse_id="session-verse",
endpoints=("cultnet://127.0.0.1:0",),
roles=("simulation-observer", "read-replica"),
shard_ids=("arena",),
))
session = CultMesh.create_game_session(
CultMesh.create_node(runtime_id="session-peer"),
CultMeshGameSessionOptions(
verse_catalog=verse_catalog,
peer_catalog=peer_catalog,
consensus_options=CultNetSimulationConsensusOptions(minimum_witnesses=2, quorum_ratio=1.0),
),
)
server = session.serve(display_name="Session Peer")
try:
raw_client = CultMesh.create_client("127.0.0.1", server.port, timeout_seconds=2.0)
hello_response = raw_client.request(
hello(runtime_id="session-prober"),
expected_schema_version="cultnet.hello.v0",
)
discovery_client = CultMesh.create_verse_discovery_client("127.0.0.1", server.port, timeout_seconds=2.0)
verses = discovery_client.fetch_verses(transport_version="cultmesh.v0")
peers = discovery_client.fetch_peers(verse_id="session-verse", roles=["simulation-observer"])
claim_hash = compute_simulation_claim_hash("hit", "alice", "bob", "frame:100")
first = raw_client.request(
simulation_observation(
message_id="session-wire-1",
witness_runtime_id="watcher-1",
shard_id="arena",
shard_epoch=4,
frame=100,
subject_id="bob",
claim_kind="hit",
claim_hash=claim_hash,
claim_summary="alice shot bob first",
),
expected_schema_version="cultnet.simulation_consensus_candidate.v0",
)
second = raw_client.request(
simulation_observation(
message_id="session-wire-2",
witness_runtime_id="watcher-2",
shard_id="arena",
shard_epoch=4,
frame=100,
subject_id="bob",
claim_kind="hit",
claim_hash=claim_hash,
claim_summary="alice shot bob first",
),
expected_schema_version="cultnet.simulation_consensus_candidate.v0",
)
finally:
server.stop()
self.assertIn("cultnet.simulation_observation.v0", hello_response["supportedMessageVersions"])
self.assertEqual(verses[0].verse_id, "session-verse")
self.assertEqual(peers[0].peer_id, "session-peer")
self.assertIn("simulation-observer", peers[0].roles)
self.assertFalse(first["hasQuorum"])
self.assertEqual(first["witnessCount"], 1)
self.assertTrue(second["hasQuorum"])
self.assertEqual(second["witnessCount"], 2)
def test_cultmesh_game_session_prediction_requires_scope_and_reconciles_shard_log(self) -> None:
note_doc = define_database_entry_type(
"mesh.input",