Serve Python game session surfaces
This commit is contained in:
parent
9b9c4e1310
commit
fdc80db44e
3 changed files with 108 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue