diff --git a/pom.xml b/pom.xml
index 1d50075..95184dd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -67,6 +67,13 @@
3.45.1.0
+
+
+ org.java-websocket
+ Java-WebSocket
+ 1.5.6
+
+
org.junit.jupiter
diff --git a/src/main/java/commanderailab/bridge/GameSession.java b/src/main/java/commanderailab/bridge/GameSession.java
new file mode 100644
index 0000000..66bb004
--- /dev/null
+++ b/src/main/java/commanderailab/bridge/GameSession.java
@@ -0,0 +1,235 @@
+package commanderailab.bridge;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+import java.util.*;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+/**
+ * GameSession — holds state for one live interactive Commander game.
+ *
+ * PHASE 1 STUB: generates synthetic game state so you can verify the full
+ * WebSocket contract round-trips correctly before wiring in real Forge IPC.
+ *
+ * Replace run() in Phase 2 once the contract is verified end-to-end.
+ */
+public class GameSession {
+
+ private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
+
+ private final List deckNames;
+ private final Long seed;
+ private final String forgeJarPath;
+ private final String forgeWorkDir;
+ private final BlockingQueue humanActionQueue;
+
+ private volatile boolean running = false;
+ private volatile int turnNumber = 1;
+
+ // Mutable game state
+ private String phase = "UNTAP";
+ private int activePlayer = 0;
+ private int priorityPlayer = 0;
+ private final int[] life;
+ private final int[] poison;
+ private final List[] hands;
+ private final List[] battlefields;
+ private final List[] graveyards;
+ private final List[] commandZones;
+ private final int[] commanderTax;
+ private boolean awaitingHumanInput = false;
+
+ private static final String[] PHASES = {
+ "UNTAP", "UPKEEP", "DRAW",
+ "MAIN1", "BEGIN_COMBAT", "DECLARE_ATTACKERS",
+ "DECLARE_BLOCKERS", "DAMAGE", "END_COMBAT",
+ "MAIN2", "END", "CLEANUP"
+ };
+ private int phaseIndex = 0;
+
+ @SuppressWarnings("unchecked")
+ public GameSession(List deckNames, Long seed,
+ String forgeJarPath, String forgeWorkDir,
+ BlockingQueue humanActionQueue) {
+ this.deckNames = deckNames;
+ this.seed = seed;
+ this.forgeJarPath = forgeJarPath;
+ this.forgeWorkDir = forgeWorkDir;
+ this.humanActionQueue = humanActionQueue;
+
+ int seats = Math.min(deckNames.size(), 4);
+ life = new int[seats];
+ poison = new int[seats];
+ hands = new List[seats];
+ battlefields = new List[seats];
+ graveyards = new List[seats];
+ commandZones = new List[seats];
+ commanderTax = new int[seats];
+
+ for (int i = 0; i < seats; i++) {
+ life[i] = 40;
+ hands[i] = new ArrayList<>(List.of(
+ "card_stub_1", "card_stub_2", "card_stub_3",
+ "card_stub_4", "card_stub_5", "card_stub_6", "card_stub_7"
+ ));
+ battlefields[i] = new ArrayList<>();
+ graveyards[i] = new ArrayList<>();
+ commandZones[i] = new ArrayList<>(List.of(deckNames.get(i) + "_commander"));
+ }
+ }
+
+ public boolean isRunning() { return running; }
+ public int getTurnNumber() { return turnNumber; }
+
+ public void stop() {
+ running = false;
+ }
+
+ /**
+ * Run the game loop. Calls onStateChange after every action resolves.
+ * Blocks until the game ends or stop() is called.
+ *
+ * PHASE 1 STUB: advances through phases automatically; on MAIN1 of seat 0's
+ * turn, pauses and waits for the human client to send an action.
+ */
+ public void run(Consumer