gameNodeRoom
Run the fixed-authority bootstrap for the game in the room named gameId over rooms — the game-per-room analogue of gameNode, and the entry point of the server-core game-per-room composition.
With placement = ConsensusPlacement.serverCore(core) on every peer:
The server (a core member) passes its us.tractat.kuilt.core.MuxServerLoom, once per game. Each call opens that game's room, wraps it in the star-relay layer, and seats a Raft cluster whose voters are core. gameNode launches the core leader's learner-admission loop against the room seam — its roster is the room, so each game admits exactly its own players (the per-room scoping that makes the core-side admission loop a proper part of this composition rather than transitional glue).
Each player passes its us.tractat.kuilt.core.MuxClientLoom with the same core and placement, and rides that game as a learner over the shared connection: committed log in, proposals forwarded to the core leader, identical consuming layer.
rooms must be a session-mux Loom — the room is selected purely by the gameId channel name on either side (us.tractat.kuilt.core.MuxClientLoom maps host and join of one name to the same channel). Do not pass a plain transport loom here; on a loom where host-vs-join is a real role split this entry point would open a new session instead of entering the room.
Remaining parameters are forwarded to gameNode unchanged; see gameNode for their semantics.
Samples
runTest(StandardTestDispatcher(), timeout = TEST_WEDGE_BACKSTOP) {
val dispatcher = requireNotNull(coroutineContext[kotlin.coroutines.ContinuationInterceptor])
val fabric = InMemoryRoomFabric(backgroundScope, dispatcher, random = Random(0))
val clock = { Instant.fromEpochMilliseconds(0) }
fun config(seed: Long) = RaftConfig(expectVirtualTime = true, random = Random(seed))
// Room "casual": appoint-the-host — the server hosts, alice takes a voter seat.
val casualHost = async {
backgroundScope.gameHostedRoom(
fabric.serverLoom, "casual", peerCount = 2,
raftConfig = config(1), random = Random(11), clock = clock,
)
}
val casualAlice = async {
backgroundScope.gameJoinRoom(
fabric.clientLoom(PeerId("alice"), Random(21)), "casual",
raftConfig = config(2), random = Random(12), clock = clock,
)
}
// Room "ranked": server-core — every voter seat is the server's; bob rides as a learner.
val core = setOf(NodeId("server"))
val placement = ConsensusPlacement.serverCore(core)
backgroundScope.gameNodeRoom(
fabric.serverLoom, "ranked", voterIds = core,
raftConfig = config(3), random = Random(13), clock = clock, placement = placement,
)
val rankedBob = backgroundScope.gameNodeRoom(
fabric.clientLoom(PeerId("bob"), Random(22)), "ranked", voterIds = core,
raftConfig = config(4), random = Random(14), clock = clock, placement = placement,
)
// Both games converge independently — same TurnSequencer code, different authority.
val casualMove = TurnSequencer(casualAlice.await().node, Int.serializer()).propose(1)
val rankedMove = TurnSequencer(rankedBob.node, Int.serializer()).propose(2)
assertEquals(1, casualMove.action)
assertEquals(2, rankedMove.action)
casualHost.await().close()
}