tinysim 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tinysim/__init__.py +11 -0
- tinysim/_tk_base.py +54 -0
- tinysim/flappy/__init__.py +103 -0
- tinysim/flappy/sim.js +118 -0
- tinysim/flappy/tk.py +94 -0
- tinysim/flappy/widget.py +53 -0
- tinysim/frogger/__init__.py +145 -0
- tinysim/frogger/sim.js +110 -0
- tinysim/frogger/tk.py +95 -0
- tinysim/frogger/widget.py +60 -0
- tinysim/mountain_car/__init__.py +56 -0
- tinysim/mountain_car/sim.js +158 -0
- tinysim/mountain_car/styles.css +5 -0
- tinysim/mountain_car/tk.py +141 -0
- tinysim/mountain_car/widget.py +56 -0
- tinysim/simple_amr/__init__.py +0 -0
- tinysim/simple_amr/example_maps.py +121 -0
- tinysim/simple_amr/sim.js +430 -0
- tinysim/simple_amr/styles.css +54 -0
- tinysim/simple_amr/widget.py +73 -0
- tinysim/topdown_driving/__init__.py +190 -0
- tinysim/topdown_driving/sim.js +136 -0
- tinysim/topdown_driving/tk.py +180 -0
- tinysim/topdown_driving/track_0.json +753 -0
- tinysim/topdown_driving/widget.py +60 -0
- tinysim-0.0.1.dist-info/METADATA +56 -0
- tinysim-0.0.1.dist-info/RECORD +33 -0
- tinysim-0.0.1.dist-info/WHEEL +5 -0
- tinysim-0.0.1.dist-info/top_level.txt +2 -0
- tinysim_warp/cart_pole/__init__.py +259 -0
- tinysim_warp/quadruped/__init__.py +203 -0
- tinysim_warp/simple_quadruped/__init__.py +168 -0
- tinysim_warp/simple_quadruped/simple_quadruped.urdf +247 -0
tinysim/__init__.py
ADDED
tinysim/_tk_base.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
import threading
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TkBaseFrontend(ABC):
|
|
7
|
+
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self._root = None
|
|
10
|
+
self._canvas = None
|
|
11
|
+
self._thread = None
|
|
12
|
+
|
|
13
|
+
def render(self):
|
|
14
|
+
if self._thread is not None:
|
|
15
|
+
return
|
|
16
|
+
self._thread = threading.Thread(target=self._window_hook, daemon=True)
|
|
17
|
+
self._thread.start()
|
|
18
|
+
|
|
19
|
+
def _window_hook(self):
|
|
20
|
+
root = tk.Tk()
|
|
21
|
+
root.protocol("WM_DELETE_WINDOW", self._on_close)
|
|
22
|
+
self._create_window(root)
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def _create_window(self, root):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def bring_to_front(self, root):
|
|
29
|
+
root.lift()
|
|
30
|
+
root.attributes("-topmost", True)
|
|
31
|
+
root.after_idle(root.attributes, "-topmost", False)
|
|
32
|
+
root.focus_force()
|
|
33
|
+
|
|
34
|
+
def _on_close(self):
|
|
35
|
+
if self._root:
|
|
36
|
+
try:
|
|
37
|
+
self._root.destroy()
|
|
38
|
+
except tk.TclError:
|
|
39
|
+
pass
|
|
40
|
+
self._root = None
|
|
41
|
+
self._canvas = None
|
|
42
|
+
|
|
43
|
+
def _pump(self):
|
|
44
|
+
if not self._root:
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
self._root.update_idletasks()
|
|
49
|
+
self._root.update()
|
|
50
|
+
except tk.TclError:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
if self._root:
|
|
54
|
+
self._root.after(20, self._pump)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .. import SimEnvironment
|
|
3
|
+
|
|
4
|
+
WIDTH, HEIGHT = 800, 600
|
|
5
|
+
GRAVITY = 900.0
|
|
6
|
+
FLAP_STRENGTH = -300.0
|
|
7
|
+
PIPE_SPEED = -200.0
|
|
8
|
+
PIPE_WIDTH = 80
|
|
9
|
+
PIPE_GAP = 200
|
|
10
|
+
PIPE_INTERVAL = 1.6
|
|
11
|
+
|
|
12
|
+
BIRD_X = 200
|
|
13
|
+
BIRD_SIZE = 35
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FlappyEnv(SimEnvironment):
|
|
17
|
+
def __init__(self, num_envs: int = 1):
|
|
18
|
+
self.num_envs = num_envs
|
|
19
|
+
self.reset()
|
|
20
|
+
|
|
21
|
+
def reset(self):
|
|
22
|
+
# birds (vectorized)
|
|
23
|
+
self.bird_y = np.full(self.num_envs, HEIGHT / 2, dtype=np.float32)
|
|
24
|
+
self.bird_vel = np.zeros(self.num_envs, dtype=np.float32)
|
|
25
|
+
self.done = np.zeros(self.num_envs, dtype=bool)
|
|
26
|
+
|
|
27
|
+
# shared pipes
|
|
28
|
+
self.pipes_x = np.empty(0, dtype=np.float32)
|
|
29
|
+
self.pipes_y = np.empty(0, dtype=np.float32)
|
|
30
|
+
self.time_since_pipe = PIPE_INTERVAL
|
|
31
|
+
return self.step(np.zeros(self.num_envs, dtype=np.int32), dt=0.0)
|
|
32
|
+
|
|
33
|
+
def _step_physics(self, action, dt):
|
|
34
|
+
flap_mask = (action == 1) & (~self.done)
|
|
35
|
+
self.bird_vel[flap_mask] = FLAP_STRENGTH
|
|
36
|
+
self.bird_vel += GRAVITY * dt
|
|
37
|
+
self.bird_y += self.bird_vel * dt
|
|
38
|
+
|
|
39
|
+
def _spawn_pipe(self):
|
|
40
|
+
pipe_y = np.random.randint(120, HEIGHT - 120 - PIPE_GAP)
|
|
41
|
+
self.pipes_x = np.append(self.pipes_x, WIDTH)
|
|
42
|
+
self.pipes_y = np.append(self.pipes_y, pipe_y)
|
|
43
|
+
|
|
44
|
+
def _update_pipes(self, dt):
|
|
45
|
+
self.time_since_pipe += dt
|
|
46
|
+
if self.time_since_pipe > PIPE_INTERVAL:
|
|
47
|
+
self.time_since_pipe = 0.0
|
|
48
|
+
self._spawn_pipe()
|
|
49
|
+
|
|
50
|
+
self.pipes_x += PIPE_SPEED * dt
|
|
51
|
+
|
|
52
|
+
# keep pipes on screen
|
|
53
|
+
keep = self.pipes_x > -PIPE_WIDTH
|
|
54
|
+
self.pipes_x = self.pipes_x[keep]
|
|
55
|
+
self.pipes_y = self.pipes_y[keep]
|
|
56
|
+
|
|
57
|
+
def _check_collisions(self): # world bounds
|
|
58
|
+
hit_bounds = (self.bird_y < 0) | (self.bird_y + BIRD_SIZE > HEIGHT)
|
|
59
|
+
bx = BIRD_X
|
|
60
|
+
by = self.bird_y[:, None]
|
|
61
|
+
px = self.pipes_x[None, :]
|
|
62
|
+
upper_y = np.zeros_like(self.pipes_y)[None, :]
|
|
63
|
+
upper_h = self.pipes_y[None, :]
|
|
64
|
+
lower_y = (self.pipes_y + PIPE_GAP)[None, :]
|
|
65
|
+
lower_h = (HEIGHT - (self.pipes_y + PIPE_GAP))[None, :]
|
|
66
|
+
x_overlap = (bx < px + PIPE_WIDTH) & (bx + BIRD_SIZE > px)
|
|
67
|
+
upper_hit = x_overlap & (by < upper_y + upper_h) & (by + BIRD_SIZE > upper_y)
|
|
68
|
+
lower_hit = x_overlap & (by < lower_y + lower_h) & (by + BIRD_SIZE > lower_y)
|
|
69
|
+
hit_pipe = (upper_hit | lower_hit).any(axis=1)
|
|
70
|
+
self.done |= hit_bounds | hit_pipe
|
|
71
|
+
|
|
72
|
+
def step(self, action, dt=0.02):
|
|
73
|
+
if np.isscalar(action):
|
|
74
|
+
action = np.full(self.num_envs, action, dtype=np.float32)
|
|
75
|
+
else:
|
|
76
|
+
action = np.asarray(action, dtype=np.float32)
|
|
77
|
+
if action.shape[0] != self.num_envs:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"Expected actions of shape ({self.num_envs},), got {action.shape}"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# mask done environments to have no action
|
|
83
|
+
action = action * (~self.done)
|
|
84
|
+
self._step_physics(action, dt)
|
|
85
|
+
self._update_pipes(dt)
|
|
86
|
+
self._check_collisions()
|
|
87
|
+
|
|
88
|
+
bird_y = self.bird_y.tolist()
|
|
89
|
+
bird_vel = self.bird_vel.tolist()
|
|
90
|
+
done = self.done.tolist()
|
|
91
|
+
|
|
92
|
+
if self.num_envs == 1:
|
|
93
|
+
bird_y = self.bird_y[0]
|
|
94
|
+
bird_vel = self.bird_vel[0]
|
|
95
|
+
done = bool(self.done[0])
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
"bird_y": bird_y,
|
|
99
|
+
"bird_vel": bird_vel,
|
|
100
|
+
"pipes_x": self.pipes_x.tolist(),
|
|
101
|
+
"pipes_y": self.pipes_y.tolist(),
|
|
102
|
+
"done": done,
|
|
103
|
+
}
|
tinysim/flappy/sim.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
let simState = {};
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
initialize({ model }) {
|
|
5
|
+
model.on("change:sim_state", () => {
|
|
6
|
+
simState = model.get("sim_state") || {};
|
|
7
|
+
});
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
async render({ model, el }) {
|
|
11
|
+
const [width, height] = model.get("_viewport_size") || [800, 600];
|
|
12
|
+
|
|
13
|
+
const container = document.createElement("div");
|
|
14
|
+
container.style.position = "relative";
|
|
15
|
+
container.style.width = width + "px";
|
|
16
|
+
container.style.height = height + "px";
|
|
17
|
+
el.appendChild(container);
|
|
18
|
+
|
|
19
|
+
const canvas = document.createElement("canvas");
|
|
20
|
+
canvas.width = width;
|
|
21
|
+
canvas.height = height;
|
|
22
|
+
container.appendChild(canvas);
|
|
23
|
+
const ctx = canvas.getContext("2d");
|
|
24
|
+
|
|
25
|
+
const WORLD_WIDTH = 800;
|
|
26
|
+
const WORLD_HEIGHT = 600;
|
|
27
|
+
const BIRD_X = 200;
|
|
28
|
+
const BIRD_SIZE = 35;
|
|
29
|
+
const PIPE_WIDTH = 80;
|
|
30
|
+
const PIPE_GAP = 200;
|
|
31
|
+
const GROUND_HEIGHT = 80;
|
|
32
|
+
|
|
33
|
+
// Scale world to canvas (in case viewport differs from 800×600)
|
|
34
|
+
const scaleX = width / WORLD_WIDTH;
|
|
35
|
+
const scaleY = height / WORLD_HEIGHT;
|
|
36
|
+
|
|
37
|
+
function draw() {
|
|
38
|
+
const state = simState || {};
|
|
39
|
+
const birdY = state.bird_y ?? WORLD_HEIGHT / 2;
|
|
40
|
+
const pipes_x = state.pipes_x || [];
|
|
41
|
+
const pipes_y = state.pipes_y || [];
|
|
42
|
+
const done = state.done || false;
|
|
43
|
+
|
|
44
|
+
ctx.clearRect(0, 0, width, height);
|
|
45
|
+
|
|
46
|
+
// Background sky
|
|
47
|
+
ctx.fillStyle = "#70C5CE";
|
|
48
|
+
ctx.fillRect(0, 0, width, height);
|
|
49
|
+
|
|
50
|
+
// Ground
|
|
51
|
+
const groundH = GROUND_HEIGHT * scaleY;
|
|
52
|
+
ctx.fillStyle = "#DED895";
|
|
53
|
+
ctx.fillRect(0, height - groundH, width, groundH);
|
|
54
|
+
|
|
55
|
+
// Bird
|
|
56
|
+
const birdScreenX = BIRD_X * scaleX;
|
|
57
|
+
const birdScreenY = birdY * scaleY;
|
|
58
|
+
const birdSizeX = BIRD_SIZE * scaleX;
|
|
59
|
+
const birdSizeY = BIRD_SIZE * scaleY;
|
|
60
|
+
|
|
61
|
+
ctx.fillStyle = "#FFD700";
|
|
62
|
+
ctx.strokeStyle = "#000000";
|
|
63
|
+
ctx.lineWidth = 1;
|
|
64
|
+
|
|
65
|
+
if (!done) {
|
|
66
|
+
ctx.beginPath();
|
|
67
|
+
ctx.ellipse(
|
|
68
|
+
birdScreenX + birdSizeX / 2,
|
|
69
|
+
birdScreenY + birdSizeY / 2,
|
|
70
|
+
birdSizeX / 2,
|
|
71
|
+
birdSizeY / 2,
|
|
72
|
+
0,
|
|
73
|
+
0,
|
|
74
|
+
Math.PI * 2
|
|
75
|
+
);
|
|
76
|
+
ctx.fill();
|
|
77
|
+
ctx.stroke();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Pipes
|
|
81
|
+
ctx.fillStyle = "#228B22";
|
|
82
|
+
ctx.strokeStyle = "#4a8d34";
|
|
83
|
+
ctx.lineWidth = 2 * ((scaleX + scaleY) / 2);
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < pipes_x.length; i++) {
|
|
86
|
+
const pipeX = pipes_x[i];
|
|
87
|
+
const pipeY = pipes_y[i];
|
|
88
|
+
|
|
89
|
+
const pw = PIPE_WIDTH * scaleX;
|
|
90
|
+
const ux = pipeX * scaleX;
|
|
91
|
+
const uy = 0; // Upper pipe starts at top of screen
|
|
92
|
+
const uh = pipeY * scaleY; // Upper pipe height extends down to pipeY
|
|
93
|
+
const lx = pipeX * scaleX;
|
|
94
|
+
const ly = (pipeY + PIPE_GAP) * scaleY; // Lower pipe starts after gap
|
|
95
|
+
const lh = (WORLD_HEIGHT - (pipeY + PIPE_GAP)) * scaleY; // Lower pipe extends to bottom
|
|
96
|
+
|
|
97
|
+
// Upper pipe
|
|
98
|
+
ctx.beginPath();
|
|
99
|
+
ctx.rect(ux, uy, pw, uh);
|
|
100
|
+
ctx.fill();
|
|
101
|
+
ctx.stroke();
|
|
102
|
+
|
|
103
|
+
// Lower pipe
|
|
104
|
+
ctx.beginPath();
|
|
105
|
+
ctx.rect(lx, ly, pw, lh);
|
|
106
|
+
ctx.fill();
|
|
107
|
+
ctx.stroke();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
requestAnimationFrame(draw);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
draw();
|
|
114
|
+
|
|
115
|
+
model.set("_view_ready", true);
|
|
116
|
+
model.save_changes();
|
|
117
|
+
}
|
|
118
|
+
};
|
tinysim/flappy/tk.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
import tkinter as tk
|
|
5
|
+
from .. import _tk_base
|
|
6
|
+
except ImportError:
|
|
7
|
+
raise ImportError("tkinter is required for FlappyTkFrontend")
|
|
8
|
+
|
|
9
|
+
from . import (
|
|
10
|
+
FlappyEnv,
|
|
11
|
+
WIDTH,
|
|
12
|
+
HEIGHT,
|
|
13
|
+
PIPE_WIDTH,
|
|
14
|
+
BIRD_SIZE,
|
|
15
|
+
BIRD_X,
|
|
16
|
+
PIPE_GAP,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FlappyTkFrontend(_tk_base.TkBaseFrontend):
|
|
21
|
+
|
|
22
|
+
def __init__(self, viewport_size=(800, 600), sim_env=None):
|
|
23
|
+
super().__init__()
|
|
24
|
+
if sim_env is None:
|
|
25
|
+
sim_env = FlappyEnv()
|
|
26
|
+
|
|
27
|
+
self.sim_env = sim_env
|
|
28
|
+
self._viewport_size = viewport_size
|
|
29
|
+
|
|
30
|
+
async def step(self, action, dt=0.02):
|
|
31
|
+
state = self.sim_env.step(action, dt=dt)
|
|
32
|
+
if self._root:
|
|
33
|
+
self._root.after(0, lambda: self._draw_state(state))
|
|
34
|
+
|
|
35
|
+
await asyncio.sleep(dt)
|
|
36
|
+
return state
|
|
37
|
+
|
|
38
|
+
async def reset(self):
|
|
39
|
+
state = self.sim_env.reset()
|
|
40
|
+
if self._root:
|
|
41
|
+
self._draw_state(state)
|
|
42
|
+
return state
|
|
43
|
+
|
|
44
|
+
def _create_window(self, root):
|
|
45
|
+
w, h = self._viewport_size
|
|
46
|
+
root.title("Flappy Bird")
|
|
47
|
+
canvas = tk.Canvas(root, width=w, height=h, bg="#1E1E1E")
|
|
48
|
+
canvas.pack(fill="both", expand=True)
|
|
49
|
+
self._root = root
|
|
50
|
+
self._canvas = canvas
|
|
51
|
+
self.bring_to_front(root)
|
|
52
|
+
self._draw_state(self.sim_env.reset())
|
|
53
|
+
self._pump()
|
|
54
|
+
root.mainloop()
|
|
55
|
+
|
|
56
|
+
def _draw_state(self, state):
|
|
57
|
+
if not self._canvas:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
canvas = self._canvas
|
|
61
|
+
canvas.delete("all")
|
|
62
|
+
canvas.create_rectangle(0, 0, WIDTH, HEIGHT, fill="#70C5CE", outline="")
|
|
63
|
+
canvas.create_rectangle(
|
|
64
|
+
0, HEIGHT - 80, WIDTH, HEIGHT, fill="#DED895", outline=""
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# bird
|
|
68
|
+
by = state["bird_y"]
|
|
69
|
+
|
|
70
|
+
if not state.get("done", False):
|
|
71
|
+
canvas.create_oval(
|
|
72
|
+
BIRD_X,
|
|
73
|
+
by,
|
|
74
|
+
BIRD_X + BIRD_SIZE,
|
|
75
|
+
by + BIRD_SIZE,
|
|
76
|
+
fill="#FFD700",
|
|
77
|
+
outline="#000",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# pipes
|
|
81
|
+
for x, y in zip(state["pipes_x"], state["pipes_y"]):
|
|
82
|
+
# Upper pipe
|
|
83
|
+
canvas.create_rectangle(
|
|
84
|
+
x, 0, x + PIPE_WIDTH, y, fill="#228B22", outline="#4a8d34"
|
|
85
|
+
)
|
|
86
|
+
# Lower pipe
|
|
87
|
+
canvas.create_rectangle(
|
|
88
|
+
x,
|
|
89
|
+
y + PIPE_GAP,
|
|
90
|
+
x + PIPE_WIDTH,
|
|
91
|
+
HEIGHT,
|
|
92
|
+
fill="#228B22",
|
|
93
|
+
outline="#4a8d34",
|
|
94
|
+
)
|
tinysim/flappy/widget.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import anywidget
|
|
3
|
+
import traitlets
|
|
4
|
+
import asyncio
|
|
5
|
+
from IPython.display import display
|
|
6
|
+
from jupyter_ui_poll import ui_events
|
|
7
|
+
|
|
8
|
+
from . import FlappyEnv
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FlappySim(anywidget.AnyWidget):
|
|
12
|
+
_esm = pathlib.Path(__file__).parent / "sim.js"
|
|
13
|
+
|
|
14
|
+
sim_state = traitlets.Dict(default_value={}).tag(sync=True)
|
|
15
|
+
_viewport_size = traitlets.Tuple(
|
|
16
|
+
traitlets.Int(), traitlets.Int(), default_value=(800, 600)
|
|
17
|
+
).tag(sync=True)
|
|
18
|
+
_manual_control = traitlets.Bool(default_value=False).tag(sync=True)
|
|
19
|
+
_view_ready = traitlets.Bool(default_value=False).tag(sync=True)
|
|
20
|
+
|
|
21
|
+
def __init__(self, viewport_size=(800, 600), manual_control=False, sim_env=None):
|
|
22
|
+
super().__init__()
|
|
23
|
+
self._viewport_size = viewport_size
|
|
24
|
+
self._manual_control = manual_control
|
|
25
|
+
if sim_env is None:
|
|
26
|
+
sim_env = FlappyEnv()
|
|
27
|
+
if sim_env.num_envs != 1:
|
|
28
|
+
raise ValueError("FlappySim currently only supports single environment.")
|
|
29
|
+
|
|
30
|
+
self.sim_env = sim_env
|
|
31
|
+
self.sim_state = self.sim_env.reset()
|
|
32
|
+
|
|
33
|
+
def render(self):
|
|
34
|
+
display(self)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
with ui_events() as ui_poll:
|
|
38
|
+
while not self._view_ready:
|
|
39
|
+
ui_poll(100)
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
async def step(self, action, dt=0.02):
|
|
44
|
+
state = self.sim_env.step(action, dt=dt)
|
|
45
|
+
self.sim_state = state
|
|
46
|
+
await asyncio.sleep(dt)
|
|
47
|
+
return state
|
|
48
|
+
|
|
49
|
+
async def reset(self):
|
|
50
|
+
state = self.sim_env.reset()
|
|
51
|
+
self.sim_state = state
|
|
52
|
+
await asyncio.sleep(0)
|
|
53
|
+
return state
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .. import SimEnvironment
|
|
3
|
+
|
|
4
|
+
WIDTH, HEIGHT = 800, 600
|
|
5
|
+
CELL = 40
|
|
6
|
+
ROWS, COLS = HEIGHT // CELL, WIDTH // CELL
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FroggerEnv(SimEnvironment):
|
|
10
|
+
def __init__(self, num_envs=1):
|
|
11
|
+
self.num_envs = num_envs
|
|
12
|
+
self.total_height = ROWS - 1
|
|
13
|
+
self.num_cars_per_lane = 4
|
|
14
|
+
self.car_width = CELL * 2
|
|
15
|
+
self.traffic_rows = np.array([4, 5, 6, 8, 9])
|
|
16
|
+
self.speeds = np.array([120, -150, 200, -180, 140], dtype=np.float32)
|
|
17
|
+
self.reset()
|
|
18
|
+
|
|
19
|
+
def reset(self):
|
|
20
|
+
# Frog positions for all environments (num_envs, 2)
|
|
21
|
+
self.frog_pos = np.tile(np.array([COLS // 2, ROWS - 1]), (self.num_envs, 1))
|
|
22
|
+
|
|
23
|
+
# Cars (num_lanes, num_cars_per_lane)
|
|
24
|
+
self.car_x = np.zeros(
|
|
25
|
+
(len(self.traffic_rows), self.num_cars_per_lane), dtype=np.float32
|
|
26
|
+
)
|
|
27
|
+
for i in range(self.num_cars_per_lane):
|
|
28
|
+
self.car_x[:, i] = i * (WIDTH // self.num_cars_per_lane)
|
|
29
|
+
|
|
30
|
+
# Score and crossings per environment
|
|
31
|
+
self.crossings = np.zeros(self.num_envs, dtype=np.float32)
|
|
32
|
+
self.score = np.zeros(self.num_envs, dtype=np.float32)
|
|
33
|
+
return self.step(np.zeros(self.num_envs, dtype=np.int32), dt=0.0)
|
|
34
|
+
|
|
35
|
+
def _build_car_grid(self):
|
|
36
|
+
grid = np.zeros((ROWS, COLS), dtype=bool)
|
|
37
|
+
for lane_idx, row in enumerate(self.traffic_rows):
|
|
38
|
+
x0 = self.car_x[lane_idx, :]
|
|
39
|
+
x1 = x0 + self.car_width
|
|
40
|
+
col_start = np.clip((x0 // CELL).astype(int), 0, COLS - 1)
|
|
41
|
+
col_end = np.clip((x1 // CELL).astype(int), 0, COLS - 1)
|
|
42
|
+
for start, end in zip(col_start, col_end):
|
|
43
|
+
grid[row, start : end + 1] = True
|
|
44
|
+
return grid
|
|
45
|
+
|
|
46
|
+
def step(self, action, dt=0.01):
|
|
47
|
+
if np.isscalar(action):
|
|
48
|
+
action = np.full(self.num_envs, action, dtype=np.float32)
|
|
49
|
+
else:
|
|
50
|
+
action = np.asarray(action, dtype=np.float32)
|
|
51
|
+
if action.shape[0] != self.num_envs:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"Expected actions of shape ({self.num_envs},), got {action.shape}"
|
|
54
|
+
)
|
|
55
|
+
action_map = {
|
|
56
|
+
0: (0, 0),
|
|
57
|
+
1: (-1, 0),
|
|
58
|
+
2: (1, 0),
|
|
59
|
+
3: (0, -1),
|
|
60
|
+
4: (0, 1),
|
|
61
|
+
}
|
|
62
|
+
dx = np.array([action_map[a][0] for a in action])
|
|
63
|
+
dy = np.array([action_map[a][1] for a in action])
|
|
64
|
+
|
|
65
|
+
# Move frogs
|
|
66
|
+
self.frog_pos[:, 0] = np.clip(self.frog_pos[:, 0] + dx, 0, COLS - 1)
|
|
67
|
+
self.frog_pos[:, 1] = np.clip(self.frog_pos[:, 1] + dy, 0, ROWS - 1)
|
|
68
|
+
|
|
69
|
+
# Update car positions
|
|
70
|
+
self.car_x += self.speeds[:, None] * dt
|
|
71
|
+
|
|
72
|
+
# Wrap cars
|
|
73
|
+
pos_mask_pos = self.speeds[:, None] > 0
|
|
74
|
+
pos_mask_neg = self.speeds[:, None] < 0
|
|
75
|
+
self.car_x[pos_mask_pos & (self.car_x > WIDTH + 50)] = -self.car_width - 50
|
|
76
|
+
self.car_x[pos_mask_neg & (self.car_x < -self.car_width - 50)] = WIDTH + 50
|
|
77
|
+
|
|
78
|
+
done = np.zeros(self.num_envs, dtype=bool)
|
|
79
|
+
frog_rects = np.stack(
|
|
80
|
+
[
|
|
81
|
+
self.frog_pos[:, 0] * CELL,
|
|
82
|
+
self.frog_pos[:, 1] * CELL,
|
|
83
|
+
np.full(self.num_envs, CELL),
|
|
84
|
+
np.full(self.num_envs, CELL),
|
|
85
|
+
],
|
|
86
|
+
axis=1,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
self.car_rects = []
|
|
90
|
+
for lane_idx, row in enumerate(self.traffic_rows):
|
|
91
|
+
self.car_rects.append(
|
|
92
|
+
np.stack(
|
|
93
|
+
[
|
|
94
|
+
self.car_x[lane_idx, :], # x positions
|
|
95
|
+
np.full(self.num_cars_per_lane, row * CELL + 8), # y positions
|
|
96
|
+
np.full(self.num_cars_per_lane, self.car_width), # widths
|
|
97
|
+
np.full(self.num_cars_per_lane, CELL - 16), # heights
|
|
98
|
+
],
|
|
99
|
+
axis=1,
|
|
100
|
+
)
|
|
101
|
+
) # shape: (num_cars_per_lane, 4)
|
|
102
|
+
|
|
103
|
+
for env_idx in range(self.num_envs):
|
|
104
|
+
ax, ay, aw, ah = frog_rects[env_idx]
|
|
105
|
+
collision = False
|
|
106
|
+
for car_rects in self.car_rects:
|
|
107
|
+
bx, by, bw, bh = (
|
|
108
|
+
car_rects[:, 0],
|
|
109
|
+
car_rects[:, 1],
|
|
110
|
+
car_rects[:, 2],
|
|
111
|
+
car_rects[:, 3],
|
|
112
|
+
)
|
|
113
|
+
overlap = (
|
|
114
|
+
(ax < bx + bw) & (ax + aw > bx) & (ay < by + bh) & (ay + ah > by)
|
|
115
|
+
)
|
|
116
|
+
if np.any(overlap):
|
|
117
|
+
collision = True
|
|
118
|
+
break
|
|
119
|
+
done[env_idx] = collision
|
|
120
|
+
|
|
121
|
+
# Handle frogs that reached the top
|
|
122
|
+
reached_top = self.frog_pos[:, 1] == 0
|
|
123
|
+
self.crossings[reached_top] += 1
|
|
124
|
+
self.frog_pos[reached_top] = [COLS // 2, ROWS - 1]
|
|
125
|
+
|
|
126
|
+
# Update score
|
|
127
|
+
current_height = self.total_height - self.frog_pos[:, 1]
|
|
128
|
+
self.score = self.crossings + current_height / self.total_height
|
|
129
|
+
|
|
130
|
+
frog_pos = self.frog_pos.tolist()
|
|
131
|
+
grid = self._build_car_grid().tolist()
|
|
132
|
+
scores = self.score.tolist()
|
|
133
|
+
done = done.tolist()
|
|
134
|
+
|
|
135
|
+
if self.num_envs == 1:
|
|
136
|
+
frog_pos = frog_pos[0]
|
|
137
|
+
done = done[0]
|
|
138
|
+
scores = scores[0]
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
"frog_pos": frog_pos,
|
|
142
|
+
"grid": grid,
|
|
143
|
+
"done": done,
|
|
144
|
+
"score": scores,
|
|
145
|
+
}
|
tinysim/frogger/sim.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
async render({ model, el }) {
|
|
3
|
+
const [width, height] = model.get("_viewport_size") || [800, 600];
|
|
4
|
+
|
|
5
|
+
const container = document.createElement("div");
|
|
6
|
+
container.style.position = "relative";
|
|
7
|
+
container.style.width = width + "px";
|
|
8
|
+
container.style.height = height + "px";
|
|
9
|
+
el.appendChild(container);
|
|
10
|
+
|
|
11
|
+
const canvas = document.createElement("canvas");
|
|
12
|
+
canvas.width = width;
|
|
13
|
+
canvas.height = height;
|
|
14
|
+
container.appendChild(canvas);
|
|
15
|
+
const ctx = canvas.getContext("2d");
|
|
16
|
+
|
|
17
|
+
// Current sim state
|
|
18
|
+
let simState = model.get("sim_state") || {};
|
|
19
|
+
let frogPos = simState.frog_pos || [0, 0]; // grid coords
|
|
20
|
+
let score = simState.score || 0;
|
|
21
|
+
let carRects = model.get("car_positions") || []; // pixel absolute coords
|
|
22
|
+
|
|
23
|
+
// Watch sim_state and car_positions
|
|
24
|
+
model.on("change:sim_state", () => {
|
|
25
|
+
simState = model.get("sim_state") || {};
|
|
26
|
+
frogPos = simState.frog_pos || frogPos;
|
|
27
|
+
score = simState.score ?? score;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
model.on("change:car_positions", () => {
|
|
31
|
+
carRects = model.get("car_positions") || carRects;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const draw = () => {
|
|
35
|
+
ctx.clearRect(0, 0, width, height);
|
|
36
|
+
|
|
37
|
+
// Background
|
|
38
|
+
ctx.fillStyle = "#101010";
|
|
39
|
+
ctx.fillRect(0, 0, width, height);
|
|
40
|
+
|
|
41
|
+
// The frog is still grid-based
|
|
42
|
+
const grid = simState.grid || [];
|
|
43
|
+
const rows = grid.length || 15;
|
|
44
|
+
const cols = (grid[0] && grid[0].length) || 20;
|
|
45
|
+
|
|
46
|
+
const cellW = width / cols;
|
|
47
|
+
const cellH = height / rows;
|
|
48
|
+
|
|
49
|
+
// Safe zones
|
|
50
|
+
ctx.fillStyle = "#000050";
|
|
51
|
+
ctx.fillRect(0, 0, width, cellH);
|
|
52
|
+
ctx.fillStyle = "#004000";
|
|
53
|
+
ctx.fillRect(0, (rows - 1) * cellH, width, cellH);
|
|
54
|
+
|
|
55
|
+
// Road tint
|
|
56
|
+
for (let r = 1; r < rows - 1; r++) {
|
|
57
|
+
ctx.fillStyle = "#202020";
|
|
58
|
+
ctx.fillRect(0, r * cellH, width, cellH);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
ctx.fillStyle = "#B43232";
|
|
62
|
+
for (let i = 0; i < carRects.length; i += 4) {
|
|
63
|
+
ctx.fillRect(carRects[i], carRects[i + 1], carRects[i + 2], carRects[i + 3]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const [frogCol, frogRow] = frogPos;
|
|
67
|
+
const fx = frogCol * cellW;
|
|
68
|
+
const fy = frogRow * cellH;
|
|
69
|
+
const radius = Math.min(cellW, cellH) * 0.4;
|
|
70
|
+
|
|
71
|
+
ctx.fillStyle = "#32DC32";
|
|
72
|
+
ctx.beginPath();
|
|
73
|
+
ctx.arc(fx + cellW / 2, fy + cellH / 2, radius, 0, Math.PI * 2);
|
|
74
|
+
ctx.fill();
|
|
75
|
+
|
|
76
|
+
// Grid lines
|
|
77
|
+
ctx.strokeStyle = "#282828";
|
|
78
|
+
ctx.lineWidth = 1;
|
|
79
|
+
|
|
80
|
+
for (let r = 0; r <= rows; r++) {
|
|
81
|
+
const y = r * cellH;
|
|
82
|
+
ctx.beginPath();
|
|
83
|
+
ctx.moveTo(0, y);
|
|
84
|
+
ctx.lineTo(width, y);
|
|
85
|
+
ctx.stroke();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (let c = 0; c <= cols; c++) {
|
|
89
|
+
const x = c * cellW;
|
|
90
|
+
ctx.beginPath();
|
|
91
|
+
ctx.moveTo(x, 0);
|
|
92
|
+
ctx.lineTo(x, height);
|
|
93
|
+
ctx.stroke();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Score
|
|
97
|
+
ctx.fillStyle = "#FFFFFF";
|
|
98
|
+
ctx.font = "16px Arial";
|
|
99
|
+
ctx.textBaseline = "top";
|
|
100
|
+
ctx.fillText(`Score: ${score.toFixed(2)}`, 10, 10);
|
|
101
|
+
|
|
102
|
+
requestAnimationFrame(draw);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
draw();
|
|
106
|
+
|
|
107
|
+
model.set("_view_ready", true);
|
|
108
|
+
model.save_changes();
|
|
109
|
+
},
|
|
110
|
+
};
|