ARGOS-MUKHTAR-Lab / doom_runtime.py
Vsevolod / ARGOS
Add Doom Coral smoke and real-robot grant proposal
b59ff36
Raw History Blame Contribute Delete
6.17 kB
"""Live ViZDoom/Freedoom runtime for ARGOS-MUKHTAR Space.
The Space owns the game world. A remote controller may only submit actions
when MUKHTAR_BRIDGE_KEY is configured and the caller supplies the same key.
No original DOOM assets are bundled; ViZDoom uses its free Freedoom assets.
"""
from __future__ import annotations
import os
import threading
import time
from typing import Any
import numpy as np
ACTION_NAMES = ("NOOP", "FORWARD", "BACK", "TURN_LEFT", "TURN_RIGHT", "FIRE")
_ACTIONS = {
"NOOP": [0, 0, 0, 0, 0],
"FORWARD": [1, 0, 0, 0, 0],
"BACK": [0, 1, 0, 0, 0],
"TURN_LEFT": [0, 0, 1, 0, 0],
"TURN_RIGHT": [0, 0, 0, 1, 0],
"FIRE": [0, 0, 0, 0, 1],
}
_LOCK = threading.RLock()
class DoomRuntime:
def __init__(self) -> None:
self.game = None
self.seed = 0
self.episode = 0
self.step_count = 0
self.total_reward = 0.0
self.last_reward = 0.0
self.last_action = "NOOP"
self.started_at = None
def _build(self, seed: int):
import vizdoom as vzd
game = vzd.DoomGame()
game.set_screen_resolution(vzd.ScreenResolution.RES_320X240)
game.set_screen_format(vzd.ScreenFormat.RGB24)
game.set_window_visible(False)
game.set_mode(vzd.Mode.PLAYER)
game.set_doom_map("map01")
game.set_doom_skill(3)
game.set_seed(int(seed))
game.set_available_buttons([
vzd.Button.MOVE_FORWARD,
vzd.Button.MOVE_BACKWARD,
vzd.Button.TURN_LEFT,
vzd.Button.TURN_RIGHT,
vzd.Button.ATTACK,
])
game.set_available_game_variables([
vzd.GameVariable.HEALTH,
vzd.GameVariable.ARMOR,
vzd.GameVariable.SELECTED_WEAPON_AMMO,
vzd.GameVariable.KILLCOUNT,
vzd.GameVariable.DEATHCOUNT,
])
game.set_living_reward(-0.001)
game.init()
return game
def start(self, seed: int = 42):
with _LOCK:
if self.game is not None:
try:
self.game.close()
except Exception:
pass
self.seed = int(seed)
self.game = self._build(self.seed)
self.game.new_episode()
self.episode += 1
self.step_count = 0
self.total_reward = 0.0
self.last_reward = 0.0
self.last_action = "NOOP"
self.started_at = time.time()
return self.snapshot()
def _ensure(self):
if self.game is None:
return self.start(self.seed or 42)
if self.game.is_episode_finished():
self.game.new_episode()
self.episode += 1
self.step_count = 0
self.total_reward = 0.0
return None
def step(self, action: str, tics: int = 4):
name = str(action or "NOOP").upper()
if name not in _ACTIONS:
raise ValueError(f"unknown Doom action: {name}")
frames = max(1, min(int(tics), 12))
with _LOCK:
self._ensure()
reward = float(self.game.make_action(_ACTIONS[name], frames))
self.step_count += 1
self.total_reward += reward
self.last_reward = reward
self.last_action = name
if self.game.is_episode_finished():
return self.snapshot(finished=True)
return self.snapshot()
def _var(self, name: str, default: float = 0.0) -> float:
import vizdoom as vzd
try:
return float(self.game.get_game_variable(getattr(vzd.GameVariable, name)))
except Exception:
return float(default)
def snapshot(self, finished: bool = False):
with _LOCK:
if self.game is None:
return None, self.telemetry(state="not_started")
state = None if self.game.is_episode_finished() else self.game.get_state()
frame = None if state is None else np.asarray(state.screen_buffer).copy()
info = self.telemetry(state="finished" if finished else "running")
return frame, info
def telemetry(self, state: str = "running") -> dict[str, Any]:
return {
"engine": "ViZDoom",
"assets": "Freedoom (free content)",
"state": state,
"seed": self.seed,
"episode": self.episode,
"step": self.step_count,
"last_action": self.last_action,
"last_reward": round(self.last_reward, 6),
"total_reward": round(self.total_reward, 6),
"health": self._var("HEALTH"),
"armor": self._var("ARMOR"),
"ammo": self._var("SELECTED_WEAPON_AMMO"),
"kills": self._var("KILLCOUNT"),
"deaths": self._var("DEATHCOUNT"),
"uptime_s": round(time.time() - self.started_at, 2)
if self.started_at else 0.0,
"remote_bridge": bridge_status(),
}
_RUNTIME = DoomRuntime()
def bridge_status() -> dict[str, Any]:
configured = bool(os.getenv("MUKHTAR_CONTROL_CODE", "").strip())
return {
"configured": configured,
"remote_write": "enabled" if configured else "disabled",
"policy": "controller-code protected; observation is read-only",
}
def doom_start(seed: int = 42):
return _RUNTIME.start(seed)
def doom_step(action: str, tics: int = 4):
return _RUNTIME.step(action, tics)
def doom_snapshot():
return _RUNTIME.snapshot()
def doom_observe():
return _RUNTIME.snapshot()
def doom_remote_step(action: str, tics: int, control_code: str):
expected = os.getenv("MUKHTAR_CONTROL_CODE", "").strip()
supplied = str(control_code or "")
frame, info = _RUNTIME.snapshot()
if not expected:
info = dict(info)
info["remote_error"] = "control_not_configured"
return frame, info
import secrets
if not secrets.compare_digest(expected, supplied):
info = dict(info)
info["remote_error"] = "unauthorized"
return frame, info
return _RUNTIME.step(action, tics)