DeltaZN commited on
Commit
7fee07b
·
1 Parent(s): 54db442

feat: disable model selector & autoplay tick by default

Browse files
README.md CHANGED
@@ -47,6 +47,10 @@ npm run dev # http://127.0.0.1:5173
47
 
48
  Controls: drag to orbit, scroll to zoom, `WASD`/arrows to pan.
49
 
 
 
 
 
50
  ## Hugging Face Space
51
 
52
  `app.py` wraps the same game runtime in `gradio.Server` and serves the built React/Three
 
47
 
48
  Controls: drag to orbit, scroll to zoom, `WASD`/arrows to pan.
49
 
50
+ Autoplay is on by default: the manual tick controls are hidden and the world
51
+ advances ticks automatically. Set `VITE_WORLD_SIMULATOR_AUTOPLAY=0` (build/dev
52
+ env) to show the manual step / play-pause controls instead.
53
+
54
  ## Hugging Face Space
55
 
56
  `app.py` wraps the same game runtime in `gradio.Server` and serves the built React/Three
dist/frontend/assets/{index-DxcXnaim.js → index-C5YBy651.js} RENAMED
The diff for this file is too large to render. See raw diff
 
dist/frontend/index.html CHANGED
@@ -4,7 +4,7 @@
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>World Simulator</title>
7
- <script type="module" crossorigin src="/world/assets/index-DxcXnaim.js"></script>
8
  <link rel="stylesheet" crossorigin href="/world/assets/index-DPzWURD0.css">
9
  </head>
10
  <body>
 
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>World Simulator</title>
7
+ <script type="module" crossorigin src="/world/assets/index-C5YBy651.js"></script>
8
  <link rel="stylesheet" crossorigin href="/world/assets/index-DPzWURD0.css">
9
  </head>
10
  <body>
frontend/src/App.tsx CHANGED
@@ -30,18 +30,20 @@ export function App() {
30
  entities={simulation.snapshot?.entities ?? []}
31
  />
32
 
33
- <SimulationControls
34
- hasSnapshot={simulation.snapshot !== null}
35
- isAutoTicking={simulation.isAutoTicking}
36
- isWaitingForTick={simulation.isWaitingForTick}
37
- isWorldCommandPending={simulation.isWorldCommandPending}
38
- onStep={() => {
39
- void simulation.step();
40
- }}
41
- onToggleAutoTick={() => {
42
- simulation.setIsAutoTicking(!simulation.isAutoTicking);
43
- }}
44
- />
 
 
45
 
46
  <AgentPanel
47
  entity={simulation.selectedEntity}
@@ -49,12 +51,7 @@ export function App() {
49
  resource={simulation.selectedResource}
50
  house={simulation.selectedHouse}
51
  entities={simulation.snapshot?.entities ?? []}
52
- modelProfiles={simulation.modelProfiles}
53
- isModelSwitchPending={simulation.isModelSwitchPending}
54
  onSelectEntity={simulation.setSelectedId}
55
- onSetNpcModel={(npcId, profileId) => {
56
- void simulation.setNpcModel(npcId, profileId);
57
- }}
58
  />
59
 
60
  <StatusToasts
 
30
  entities={simulation.snapshot?.entities ?? []}
31
  />
32
 
33
+ {simulation.hideTickControls ? null : (
34
+ <SimulationControls
35
+ hasSnapshot={simulation.snapshot !== null}
36
+ isAutoTicking={simulation.isAutoTicking}
37
+ isWaitingForTick={simulation.isWaitingForTick}
38
+ isWorldCommandPending={simulation.isWorldCommandPending}
39
+ onStep={() => {
40
+ void simulation.step();
41
+ }}
42
+ onToggleAutoTick={() => {
43
+ simulation.setIsAutoTicking(!simulation.isAutoTicking);
44
+ }}
45
+ />
46
+ )}
47
 
48
  <AgentPanel
49
  entity={simulation.selectedEntity}
 
51
  resource={simulation.selectedResource}
52
  house={simulation.selectedHouse}
53
  entities={simulation.snapshot?.entities ?? []}
 
 
54
  onSelectEntity={simulation.setSelectedId}
 
 
 
55
  />
56
 
57
  <StatusToasts
frontend/src/components/AgentPanel.tsx CHANGED
@@ -2,7 +2,6 @@ import type {
2
  BeastSnapshot,
3
  EntitySnapshot,
4
  HouseSnapshot,
5
- ModelProfileSnapshot,
6
  ResourceNodeSnapshot,
7
  } from "../types";
8
  import { TooltipLabel } from "./TooltipLabel";
@@ -13,10 +12,7 @@ type AgentPanelProps = {
13
  resource?: ResourceNodeSnapshot | null;
14
  house?: HouseSnapshot | null;
15
  entities?: EntitySnapshot[];
16
- modelProfiles?: ModelProfileSnapshot[];
17
- isModelSwitchPending?: boolean;
18
  onSelectEntity?: (id: string) => void;
19
- onSetNpcModel?: (npcId: string, profileId: string) => void;
20
  };
21
 
22
  export function AgentPanel({
@@ -25,10 +21,7 @@ export function AgentPanel({
25
  resource = null,
26
  house = null,
27
  entities = [],
28
- modelProfiles = [],
29
- isModelSwitchPending = false,
30
  onSelectEntity,
31
- onSetNpcModel,
32
  }: AgentPanelProps) {
33
  if (beast) {
34
  return <BeastPanel beast={beast} />;
@@ -51,16 +44,7 @@ export function AgentPanel({
51
  <TooltipLabel className="panelTitle" value={selectedName} />
52
  <TooltipLabel className="panelRole" value={selectedRole} />
53
  </div>
54
- {entity ? (
55
- <AgentDetails
56
- entity={entity}
57
- modelProfiles={modelProfiles}
58
- isModelSwitchPending={isModelSwitchPending}
59
- onSetNpcModel={onSetNpcModel}
60
- />
61
- ) : (
62
- <EmptyAgentReadout />
63
- )}
64
  </aside>
65
  );
66
  }
@@ -203,26 +187,12 @@ function HouseResidentList({ label, ids, emptyLabel, entities, onSelectEntity }:
203
 
204
  type AgentDetailsProps = {
205
  entity: EntitySnapshot;
206
- modelProfiles: ModelProfileSnapshot[];
207
- isModelSwitchPending: boolean;
208
- onSetNpcModel?: (npcId: string, profileId: string) => void;
209
  };
210
 
211
- function AgentDetails({
212
- entity,
213
- modelProfiles,
214
- isModelSwitchPending,
215
- onSetNpcModel,
216
- }: AgentDetailsProps) {
217
  return (
218
  <>
219
  <AgentReadout entity={entity} />
220
- <ModelSwitcher
221
- entity={entity}
222
- profiles={modelProfiles}
223
- isPending={isModelSwitchPending}
224
- onSetNpcModel={onSetNpcModel}
225
- />
226
  <SurvivalReadout entity={entity} />
227
  <ThinkingBlock entity={entity} />
228
  <RelationshipList entity={entity} />
@@ -231,39 +201,6 @@ function AgentDetails({
231
  );
232
  }
233
 
234
- type ModelSwitcherProps = {
235
- entity: EntitySnapshot;
236
- profiles: ModelProfileSnapshot[];
237
- isPending: boolean;
238
- onSetNpcModel?: (npcId: string, profileId: string) => void;
239
- };
240
-
241
- function ModelSwitcher({ entity, profiles, isPending, onSetNpcModel }: ModelSwitcherProps) {
242
- if (profiles.length === 0) {
243
- return null;
244
- }
245
-
246
- const currentProfile = entity.model_profile_id ?? entity.connector_id ?? "default";
247
-
248
- return (
249
- <label className="modelControl">
250
- <span>Model</span>
251
- <select
252
- value={currentProfile}
253
- disabled={isPending || !onSetNpcModel}
254
- onChange={(event) => onSetNpcModel?.(entity.id, event.currentTarget.value)}
255
- >
256
- {profiles.map((profile) => (
257
- <option key={profile.id} value={profile.id}>
258
- {profile.label || profile.id}
259
- {profile.model ? ` - ${profile.model}` : ""}
260
- </option>
261
- ))}
262
- </select>
263
- </label>
264
- );
265
- }
266
-
267
  function SurvivalReadout({ entity }: { entity: EntitySnapshot }) {
268
  const { hunger, fear, safety, age, max_age, importance, goal, inventory } = entity.state;
269
  if (
 
2
  BeastSnapshot,
3
  EntitySnapshot,
4
  HouseSnapshot,
 
5
  ResourceNodeSnapshot,
6
  } from "../types";
7
  import { TooltipLabel } from "./TooltipLabel";
 
12
  resource?: ResourceNodeSnapshot | null;
13
  house?: HouseSnapshot | null;
14
  entities?: EntitySnapshot[];
 
 
15
  onSelectEntity?: (id: string) => void;
 
16
  };
17
 
18
  export function AgentPanel({
 
21
  resource = null,
22
  house = null,
23
  entities = [],
 
 
24
  onSelectEntity,
 
25
  }: AgentPanelProps) {
26
  if (beast) {
27
  return <BeastPanel beast={beast} />;
 
44
  <TooltipLabel className="panelTitle" value={selectedName} />
45
  <TooltipLabel className="panelRole" value={selectedRole} />
46
  </div>
47
+ {entity ? <AgentDetails entity={entity} /> : <EmptyAgentReadout />}
 
 
 
 
 
 
 
 
 
48
  </aside>
49
  );
50
  }
 
187
 
188
  type AgentDetailsProps = {
189
  entity: EntitySnapshot;
 
 
 
190
  };
191
 
192
+ function AgentDetails({ entity }: AgentDetailsProps) {
 
 
 
 
 
193
  return (
194
  <>
195
  <AgentReadout entity={entity} />
 
 
 
 
 
 
196
  <SurvivalReadout entity={entity} />
197
  <ThinkingBlock entity={entity} />
198
  <RelationshipList entity={entity} />
 
201
  );
202
  }
203
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  function SurvivalReadout({ entity }: { entity: EntitySnapshot }) {
205
  const { hunger, fear, safety, age, max_age, importance, goal, inventory } = entity.state;
206
  if (
frontend/src/hooks/useWorldSimulation.ts CHANGED
@@ -9,10 +9,17 @@ import {
9
  } from "../api";
10
  import type { GameLogEventSnapshot, ModelProfileSnapshot, WorldSnapshot } from "../types";
11
 
 
 
 
 
 
 
 
12
  export function useWorldSimulation() {
13
  const [snapshot, setSnapshot] = useState<WorldSnapshot | null>(null);
14
  const [isTickPending, setIsTickPending] = useState(false);
15
- const [isAutoTicking, setIsAutoTicking] = useState(false);
16
  const [isModelSwitchPending, setIsModelSwitchPending] = useState(false);
17
  const [modelProfiles, setModelProfiles] = useState<ModelProfileSnapshot[]>([]);
18
  const [error, setError] = useState<string | null>(null);
@@ -96,6 +103,7 @@ export function useWorldSimulation() {
96
  setIsAutoTicking,
97
  });
98
  useModelStatusPolling(snapshot, refresh, setError);
 
99
 
100
  const selectedEntity = useMemo(
101
  () => snapshot?.entities.find((entity) => entity.id === selectedId) ?? null,
@@ -120,6 +128,7 @@ export function useWorldSimulation() {
120
  return {
121
  error,
122
  eventHistory,
 
123
  isAutoTicking,
124
  isModelSwitchPending,
125
  isWaitingForTick,
@@ -308,6 +317,23 @@ function useAutoTicks(options: AutoTickOptions) {
308
  ]);
309
  }
310
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  function useModelStatusPolling(
312
  snapshot: WorldSnapshot | null,
313
  refresh: () => Promise<void>,
 
9
  } from "../api";
10
  import type { GameLogEventSnapshot, ModelProfileSnapshot, WorldSnapshot } from "../types";
11
 
12
+ // When enabled, the manual tick controls are hidden and the world advances on its
13
+ // own. Defaults to on; set VITE_WORLD_SIMULATOR_AUTOPLAY=0/false/no to show the
14
+ // manual controls instead.
15
+ const AUTOPLAY = !["0", "false", "no"].includes(
16
+ String(import.meta.env.VITE_WORLD_SIMULATOR_AUTOPLAY ?? "").toLowerCase(),
17
+ );
18
+
19
  export function useWorldSimulation() {
20
  const [snapshot, setSnapshot] = useState<WorldSnapshot | null>(null);
21
  const [isTickPending, setIsTickPending] = useState(false);
22
+ const [isAutoTicking, setIsAutoTicking] = useState(AUTOPLAY);
23
  const [isModelSwitchPending, setIsModelSwitchPending] = useState(false);
24
  const [modelProfiles, setModelProfiles] = useState<ModelProfileSnapshot[]>([]);
25
  const [error, setError] = useState<string | null>(null);
 
103
  setIsAutoTicking,
104
  });
105
  useModelStatusPolling(snapshot, refresh, setError);
106
+ useAutoplayRearm(isAutoTicking, snapshot !== null, setIsAutoTicking);
107
 
108
  const selectedEntity = useMemo(
109
  () => snapshot?.entities.find((entity) => entity.id === selectedId) ?? null,
 
128
  return {
129
  error,
130
  eventHistory,
131
+ hideTickControls: AUTOPLAY,
132
  isAutoTicking,
133
  isModelSwitchPending,
134
  isWaitingForTick,
 
317
  ]);
318
  }
319
 
320
+ // In autoplay mode the manual controls are hidden, so a transient error that
321
+ // pauses auto-ticking must be recovered automatically; otherwise the world would
322
+ // halt with no way to restart it.
323
+ function useAutoplayRearm(
324
+ isAutoTicking: boolean,
325
+ hasSnapshot: boolean,
326
+ setIsAutoTicking: (value: boolean) => void,
327
+ ) {
328
+ useEffect(() => {
329
+ if (!AUTOPLAY || isAutoTicking || !hasSnapshot) {
330
+ return undefined;
331
+ }
332
+ const timeoutId = window.setTimeout(() => setIsAutoTicking(true), 2000);
333
+ return () => window.clearTimeout(timeoutId);
334
+ }, [isAutoTicking, hasSnapshot, setIsAutoTicking]);
335
+ }
336
+
337
  function useModelStatusPolling(
338
  snapshot: WorldSnapshot | null,
339
  refresh: () => Promise<void>,
src/world_simulator/simulation/connectors/openai_compatible.py CHANGED
@@ -1156,11 +1156,15 @@ def _survival_briefing_lines(
1156
  else:
1157
  lines.append("THREATS NEAR YOU: none.")
1158
 
1159
- # --- allies ------------------------------------------------------------ #
 
1160
  allies = _survival_nearby_allies(world, npc)
1161
  lines.append("")
1162
  if allies:
1163
- lines.append("ALLIES NEAR YOU (talk to coordinate; <=12 units to speak):")
 
 
 
1164
  for a in allies:
1165
  ap = _npc_position(world, a["id"])
1166
  where = (
@@ -1173,7 +1177,31 @@ def _survival_briefing_lines(
1173
  f"HP {a['hp']}, hunger {a['hunger']}."
1174
  )
1175
  else:
1176
- lines.append("ALLIES NEAR YOU: none in sight.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1177
 
1178
  # --- resources --------------------------------------------------------- #
1179
  resources = _survival_reachable_resources(world, npc)
@@ -1468,36 +1496,59 @@ def _survival_nearby_threats(world: WorldState, npc: Npc) -> list[dict[str, Any]
1468
  return sorted(threats, key=lambda item: (item["distance"], item["id"]))[:5]
1469
 
1470
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1471
  def _survival_nearby_allies(world: WorldState, npc: Npc) -> list[dict[str, Any]]:
 
 
1472
  allies: list[dict[str, Any]] = []
1473
  for other in world.living_npcs():
1474
  if other.id == npc.id or has_hostile_intent(other):
1475
  continue
 
 
1476
  distance = vec_distance(npc.position, other.position)
1477
  if distance > VISIBLE_RADIUS:
1478
  continue
1479
- allies.append(
1480
- {
1481
- "id": other.id,
1482
- "name": other.name,
1483
- "role": other.role,
1484
- "country_id": other.country_id,
1485
- "distance": round(distance, 2),
1486
- "trust": round(npc.relationships.get(other.id, 0.0), 2),
1487
- "hp": round(other.health),
1488
- "hunger": round(other.hunger),
1489
- "inventory": {
1490
- "food": other.inventory_food,
1491
- "herbs": other.inventory_herbs,
1492
- "wood": other.inventory_wood,
1493
- "weapon": other.inventory_weapon,
1494
- "coins": other.inventory_coins,
1495
- },
1496
- }
1497
- )
1498
  return sorted(allies, key=lambda item: (item["distance"], item["id"]))[:5]
1499
 
1500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1501
  def _survival_reachable_resources(world: WorldState, npc: Npc) -> list[dict[str, Any]]:
1502
  resources: list[dict[str, Any]] = []
1503
  for node in world.resource_nodes:
 
1156
  else:
1157
  lines.append("THREATS NEAR YOU: none.")
1158
 
1159
+ # --- allies (your own nation) ------------------------------------------ #
1160
+ own_nation = country.name if country else "your nation"
1161
  allies = _survival_nearby_allies(world, npc)
1162
  lines.append("")
1163
  if allies:
1164
+ lines.append(
1165
+ f"ALLIES NEAR YOU - your own nation {own_nation} (talk to coordinate; "
1166
+ "<=12 units to speak; do NOT attack them):"
1167
+ )
1168
  for a in allies:
1169
  ap = _npc_position(world, a["id"])
1170
  where = (
 
1177
  f"HP {a['hp']}, hunger {a['hunger']}."
1178
  )
1179
  else:
1180
+ lines.append(f"ALLIES NEAR YOU (your nation {own_nation}): none in sight.")
1181
+
1182
+ # --- enemy citizens (the rival nation) --------------------------------- #
1183
+ rival_name = rival.name if rival is not None else "the enemy nation"
1184
+ enemies = _survival_nearby_enemies(world, npc)
1185
+ lines.append("")
1186
+ if enemies:
1187
+ lines.append(
1188
+ f"ENEMY CITIZENS NEAR YOU - the rival nation {rival_name} (NOT your "
1189
+ "people; you may rob, raid, or parley with them):"
1190
+ )
1191
+ for e in enemies:
1192
+ ep = _npc_position(world, e["id"])
1193
+ where = (
1194
+ f"{_bearing_to(pos, ep.x, ep.z)} ({_loc(ep.x, ep.z)})"
1195
+ if ep is not None
1196
+ else f"{e['distance']:.0f} units away"
1197
+ )
1198
+ carrying = e["inventory"].get("coins", 0)
1199
+ lines.append(
1200
+ f" - {e['name']} \"{e['id']}\" {where}. HP {e['hp']}, "
1201
+ f"carries {carrying} coins."
1202
+ )
1203
+ else:
1204
+ lines.append(f"ENEMY CITIZENS NEAR YOU (rival nation {rival_name}): none in sight.")
1205
 
1206
  # --- resources --------------------------------------------------------- #
1207
  resources = _survival_reachable_resources(world, npc)
 
1496
  return sorted(threats, key=lambda item: (item["distance"], item["id"]))[:5]
1497
 
1498
 
1499
+ def _survival_nearby_npc_entry(npc: Npc, other: Npc, distance: float) -> dict[str, Any]:
1500
+ return {
1501
+ "id": other.id,
1502
+ "name": other.name,
1503
+ "role": other.role,
1504
+ "country_id": other.country_id,
1505
+ "distance": round(distance, 2),
1506
+ "trust": round(npc.relationships.get(other.id, 0.0), 2),
1507
+ "hp": round(other.health),
1508
+ "hunger": round(other.hunger),
1509
+ "inventory": {
1510
+ "food": other.inventory_food,
1511
+ "herbs": other.inventory_herbs,
1512
+ "wood": other.inventory_wood,
1513
+ "weapon": other.inventory_weapon,
1514
+ "coins": other.inventory_coins,
1515
+ },
1516
+ }
1517
+
1518
+
1519
  def _survival_nearby_allies(world: WorldState, npc: Npc) -> list[dict[str, Any]]:
1520
+ """Nearby NPCs of the SAME nation (true allies). Enemy-nation NPCs are listed
1521
+ separately by _survival_nearby_enemies so the two are never confused."""
1522
  allies: list[dict[str, Any]] = []
1523
  for other in world.living_npcs():
1524
  if other.id == npc.id or has_hostile_intent(other):
1525
  continue
1526
+ if other.country_id != npc.country_id:
1527
+ continue
1528
  distance = vec_distance(npc.position, other.position)
1529
  if distance > VISIBLE_RADIUS:
1530
  continue
1531
+ allies.append(_survival_nearby_npc_entry(npc, other, distance))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1532
  return sorted(allies, key=lambda item: (item["distance"], item["id"]))[:5]
1533
 
1534
 
1535
+ def _survival_nearby_enemies(world: WorldState, npc: Npc) -> list[dict[str, Any]]:
1536
+ """Nearby NPCs of the RIVAL nation - people you may rob, raid, or parley with.
1537
+ Listed whether or not they are currently hostile, so the NPC always knows who
1538
+ around it belongs to the enemy."""
1539
+ enemies: list[dict[str, Any]] = []
1540
+ for other in world.living_npcs():
1541
+ if other.id == npc.id or not npc.country_id or not other.country_id:
1542
+ continue
1543
+ if other.country_id == npc.country_id:
1544
+ continue
1545
+ distance = vec_distance(npc.position, other.position)
1546
+ if distance > VISIBLE_RADIUS:
1547
+ continue
1548
+ enemies.append(_survival_nearby_npc_entry(npc, other, distance))
1549
+ return sorted(enemies, key=lambda item: (item["distance"], item["id"]))[:5]
1550
+
1551
+
1552
  def _survival_reachable_resources(world: WorldState, npc: Npc) -> list[dict[str, Any]]:
1553
  resources: list[dict[str, Any]] = []
1554
  for node in world.resource_nodes: