File size: 8,306 Bytes
f1dbe16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1df31cf
 
 
 
 
 
d2cf491
1df31cf
 
 
f1dbe16
 
9dd0609
f1dbe16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1df31cf
 
 
 
 
 
 
 
f1dbe16
9dd0609
f1dbe16
 
 
 
 
 
9dd0609
f1dbe16
 
 
84b3ded
f1dbe16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d2cf491
f1dbe16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
"""

DOOM-FlyWire Agent

==================

Closed-loop autonomous agent connecting the ViZDoom environment to the

139,248-neuron FlyWire connectome.

"""

import os
import sys
import io
import base64
import time
import numpy as np
from PIL import Image
import vizdoom as vzd
import torch

from .connectome_engine import ConnectomeEngine
from .retinotopy_encoder import RetinotopyEncoder
from .motor_decoder import MotorDecoder

class DoomConnectomeAgent:
    def __init__(

        self,

        scenario_name: str = "defend_the_center.cfg",

        window_visible: bool = False,

        device: str = "cuda"

    ):
        print(f"[DoomAgent] Initializing DOOM-FlyWire Agent on {device}...")
        self.device = device
        
        # 1. Connectome Neural Engine (100% FlyWire)
        self.engine = ConnectomeEngine(device=device)

        # 2. Retinotopy Encoder
        self.encoder = RetinotopyEncoder(
            num_neurons=self.engine.num_neurons,
            optic_left_indices=self.engine.optic_left_indices,
            optic_right_indices=self.engine.optic_right_indices,
            t4t5_indices=self.engine.t4t5_indices,
            eye_res=32,
            device=device
        )

        # 3. ViZDoom Game Instance
        self.game = vzd.DoomGame()
        scenario_path = os.path.join(vzd.scenarios_path, scenario_name)
        self.game.load_config(scenario_path)
        self.game.set_window_visible(window_visible)
        self.game.set_screen_resolution(vzd.ScreenResolution.RES_320X240)
        self.game.set_screen_format(vzd.ScreenFormat.RGB24)
        self.game.set_labels_buffer_enabled(True)
        
        # Ensure key game variables are accessible
        try:
            self.game.add_available_game_variable(vzd.GameVariable.KILLCOUNT)
            self.game.add_available_game_variable(vzd.GameVariable.HEALTH)
            self.game.add_available_game_variable(vzd.GameVariable.AMMO2)
        except Exception:
            pass

        self.game.init()
        self.game.new_episode()
        # Enable Infinite Ammo
        try:
            self.game.send_game_command("sv_infiniteammo 1")
        except Exception:
            pass

        # Extract available buttons from ViZDoom
        available_buttons = [b.name for b in self.game.get_available_buttons()]
        print(f"[DoomAgent] ViZDoom Scenario '{scenario_name}' loaded. Buttons: {available_buttons}")

        # 4. Motor Decoder (Pure Linear Synaptic Readout)
        self.decoder = MotorDecoder(
            dn_left_indices=self.engine.dn_left_indices,
            dn_right_indices=self.engine.dn_right_indices,
            available_actions=available_buttons,
            device=device
        )

        self.step_count = 0
        self.episode_kills = 0
        self.total_kills = 0
        self.episode_reward = 0.0
        self.total_reward = 0.0
        self.is_game_over = False

        # Capture initial frame immediately so last_frame_b64 is never empty
        init_state = self.game.get_state()
        if init_state is not None:
            pil_img = Image.fromarray(init_state.screen_buffer)
            buf = io.BytesIO()
            pil_img.save(buf, format="JPEG", quality=55)
            self.last_frame_b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
        else:
            self.last_frame_b64 = ""

    def update_sandbox(self, params_dict: dict):
        """Allows live adjustments of biophysical simulation parameters."""
        self.engine.set_parameters(params_dict)

    def reset_episode(self):
        """Manually resets the game episode upon user command."""
        self.game.new_episode()
        try:
            self.game.send_game_command("sv_infiniteammo 1")
        except Exception:
            pass
        self.engine.reset_state()
        self.episode_reward = 0.0
        self.episode_kills = 0
        self.is_game_over = False

    def step(self) -> dict:
        """

        Executes one full perception-cognition-action loop:

        DOOM Frame -> Optic Lobe Current -> 15M Synapses -> Descending Neurons -> DOOM Action.

        Perpetual autonomous gameplay: automatically starts a new round when the fly dies.

        """
        if self.game.is_episode_finished() or (self.game.get_state() is not None and self.game.get_game_variable(vzd.GameVariable.HEALTH) <= 0):
            self.game.new_episode()
            try:
                self.game.send_game_command("sv_infiniteammo 1")
            except Exception:
                pass
            self.episode_reward = 0.0
            self.episode_kills = 0
            self.is_game_over = False

        state = self.game.get_state()
        if state is None:
            self.game.new_episode()
            try:
                self.game.send_game_command("sv_infiniteammo 1")
            except Exception:
                pass
            state = self.game.get_state()
            if state is None:
                return {}

        # 1. Visual Perception (Compound Eye & Target Tracking)
        screen_rgb = state.screen_buffer  # (H, W, 3) RGB
        sensory_current, visual_meta = self.encoder.encode(screen_rgb, state.labels, params=self.engine.params)

        # 2. Whole-Brain Biological Propagation (15M synapses)
        act, telemetry = self.engine.step(sensory_current)

        # 3. Motor Action Decoding (DN Population Motor Readout)
        action_binary, action_probs = self.decoder.decode(act, visual_meta, params=self.engine.params)

        # 4. Execute in DOOM (2 frame skip for natural, smooth, non-hyper speed)
        reward = self.game.make_action(action_binary, 1)
        self.episode_reward += reward
        self.total_reward += reward
        self.step_count += 1

        # Check kills / game variables
        kills = self.game.get_game_variable(vzd.GameVariable.KILLCOUNT) if vzd.GameVariable.KILLCOUNT in self.game.get_available_game_variables() else 0
        self.episode_kills = kills
        health = self.game.get_game_variable(vzd.GameVariable.HEALTH) if vzd.GameVariable.HEALTH in self.game.get_available_game_variables() else 100
        ammo = 999  # Infinite Ammo active

        if health <= 0 or self.game.is_episode_finished():
            self.total_kills += self.episode_kills
            self.game.new_episode()
            try:
                self.game.send_game_command("sv_infiniteammo 1")
            except Exception:
                pass
            self.episode_kills = 0
            self.episode_reward = 0.0
            self.is_game_over = False

        # Encode crisp 640x480 frame as lightweight JPEG base64
        pil_img = Image.fromarray(screen_rgb)
        buffer = io.BytesIO()
        pil_img.save(buffer, format="JPEG", quality=55)
        img_b64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
        self.last_frame_b64 = img_b64

        return {
            "step": self.step_count,
            "reward": reward,
            "episode_reward": self.episode_reward,
            "total_reward": self.episode_reward,
            "health": max(0, health),
            "ammo": ammo,
            "kills": kills,
            "is_game_over": self.is_game_over,
            "telemetry": telemetry,
            "visual_meta": visual_meta,
            "action_taken": [name for name, val in zip(self.decoder.available_actions, action_binary) if val == 1],
            "action_probs": action_probs,
            "frame_b64": img_b64
        }


    def close(self):
        self.game.close()

if __name__ == "__main__":
    agent = DoomConnectomeAgent(window_visible=False)
    print("Testing 50 steps of closed-loop DOOM-FlyWire gameplay...")
    t0 = time.perf_counter()
    for s in range(50):
        res = agent.step()
        if s % 10 == 0:
            print(f"Step {s:3d} | Action: {res['action_taken']} | Telemetry: Optic={res['telemetry']['optic_left']:.2f}/{res['telemetry']['optic_right']:.2f} | Brain={res['telemetry']['whole_brain_firing']:.3f}")
    agent.close()
    elapsed = time.perf_counter() - t0
    print(f"50 closed-loop game steps completed in {elapsed:.2f}s ({50/elapsed:.1f} FPS)!")