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/frogger/tk.py ADDED
@@ -0,0 +1,95 @@
1
+ import asyncio
2
+ from . import FroggerEnv, WIDTH, HEIGHT, CELL, ROWS, COLS
3
+
4
+ try:
5
+ import tkinter as tk
6
+ from .. import _tk_base
7
+ except ImportError:
8
+ raise ImportError("tkinter is required for FroggerTkFrontend")
9
+
10
+
11
+ class FroggerTkFrontend(_tk_base.TkBaseFrontend):
12
+
13
+ def __init__(self, viewport_size=(800, 600), sim_env=None):
14
+ super().__init__()
15
+ if sim_env is None:
16
+ sim_env = FroggerEnv()
17
+ if sim_env.num_envs != 1:
18
+ raise ValueError(
19
+ "FroggerTkFrontend currently only supports single environment."
20
+ )
21
+ self.sim_env = sim_env
22
+ self._viewport_size = viewport_size
23
+
24
+ async def step(self, action, dt=0.01):
25
+ state = self.sim_env.step(action, dt=dt)
26
+ if self._root:
27
+ self._root.after(0, lambda: self._draw_state(self.sim_env))
28
+
29
+ await asyncio.sleep(dt)
30
+ return state
31
+
32
+ async def reset(self):
33
+ state = self.sim_env.reset()
34
+ if self._canvas:
35
+ self._draw_state(self.sim_env)
36
+ return state
37
+
38
+ def _create_window(self, root):
39
+ w, h = self._viewport_size
40
+ root.title("Frogger")
41
+ canvas = tk.Canvas(root, width=w, height=h, bg="#1E1E1E")
42
+ canvas.pack(fill="both", expand=True)
43
+ self._root = root
44
+ self._canvas = canvas
45
+ self.bring_to_front(root)
46
+ self._draw_state(self.sim_env)
47
+ self._pump()
48
+ root.mainloop()
49
+
50
+ def _draw_state(self, sim_env: FroggerEnv):
51
+ if not self._canvas:
52
+ return
53
+
54
+ canvas = self._canvas
55
+ canvas.delete("all")
56
+
57
+ # safe zones
58
+ canvas.create_rectangle(0, 0, WIDTH, CELL, fill="#000050", outline="")
59
+ canvas.create_rectangle(
60
+ 0, (ROWS - 1) * CELL, WIDTH, HEIGHT, fill="#004000", outline=""
61
+ )
62
+
63
+ # cars
64
+ for lane_rects in sim_env.car_rects: # lane_rects shape: (num_cars_per_lane, 4)
65
+ for x, y, w, h in lane_rects:
66
+ canvas.create_rectangle(x, y, x + w, y + h, fill="#B43232", outline="")
67
+
68
+ # frog
69
+ fx, fy, fw, fh = (
70
+ sim_env.frog_pos[0, 0] * CELL,
71
+ sim_env.frog_pos[0, 1] * CELL,
72
+ CELL,
73
+ CELL,
74
+ )
75
+ canvas.create_oval(
76
+ fx + 5, fy + 5, fx + fw - 5, fy + fh - 5, fill="#32DC32", outline=""
77
+ )
78
+
79
+ # grid
80
+ for r in range(ROWS):
81
+ y = r * CELL
82
+ canvas.create_line(0, y, WIDTH, y, fill="#282828")
83
+ for c in range(COLS):
84
+ x = c * CELL
85
+ canvas.create_line(x, 0, x, HEIGHT, fill="#282828")
86
+
87
+ # score
88
+ canvas.create_text(
89
+ 10,
90
+ 10,
91
+ anchor="nw",
92
+ text=f"Score: {sim_env.score[0]:.2f}",
93
+ fill="white",
94
+ font=("Arial", 16),
95
+ )
@@ -0,0 +1,60 @@
1
+ import pathlib
2
+ import anywidget
3
+ import traitlets
4
+ import asyncio
5
+ import numpy as np
6
+ from IPython.display import display
7
+ from jupyter_ui_poll import ui_events
8
+
9
+ from . import FroggerEnv
10
+
11
+
12
+ class FroggerWidget(anywidget.AnyWidget):
13
+ _esm = pathlib.Path(__file__).parent / "sim.js"
14
+
15
+ sim_state = traitlets.Dict(default_value={}).tag(sync=True)
16
+ car_positions = traitlets.List(default_value=[]).tag(sync=True)
17
+ _viewport_size = traitlets.Tuple(
18
+ traitlets.Int(), traitlets.Int(), default_value=(800, 600)
19
+ ).tag(sync=True)
20
+ _view_ready = traitlets.Bool(default_value=False).tag(sync=True)
21
+
22
+ def get_car_positions(self):
23
+ return np.vstack(self.sim_env.car_rects).flatten().tolist()
24
+
25
+ def __init__(self, viewport_size=(800, 600), sim_env=None):
26
+ self._viewport_size = viewport_size
27
+ super().__init__()
28
+ if sim_env is None:
29
+ sim_env = FroggerEnv()
30
+
31
+ if sim_env.num_envs != 1:
32
+ raise ValueError(
33
+ "FroggerWidget currently only supports single environment."
34
+ )
35
+
36
+ self.sim_env = sim_env
37
+ self.sim_state = self.sim_env.reset()
38
+ self.car_positions = self.get_car_positions()
39
+
40
+ def render(self):
41
+ display(self)
42
+
43
+ try:
44
+ with ui_events() as ui_poll:
45
+ while not self._view_ready:
46
+ ui_poll(100)
47
+ except Exception:
48
+ pass
49
+
50
+ async def step(self, action: int, dt: float = 0.01) -> dict:
51
+ sim_state = self.sim_env.step(action)
52
+ self.sim_state = sim_state
53
+ self.car_positions = self.get_car_positions()
54
+ await asyncio.sleep(dt)
55
+ return sim_state
56
+
57
+ async def reset(self) -> dict:
58
+ sim_state = self.sim_env.reset()
59
+ self.sim_state = sim_state
60
+ return sim_state
@@ -0,0 +1,56 @@
1
+ import numpy as np
2
+ from .. import SimEnvironment
3
+
4
+
5
+ class MountainCarEnv(SimEnvironment):
6
+ def __init__(self, num_envs: int = 1):
7
+ self.num_envs = num_envs
8
+ # environment constants
9
+ self.min_position = -1.2
10
+ self.max_position = 0.6
11
+ self.max_speed = 0.07
12
+ self.force = 0.001
13
+ self.gravity = 0.0025
14
+ self.goal_position = 0.5
15
+
16
+ # initial state
17
+ self.position = np.full(num_envs, -0.5, dtype=np.float32)
18
+ self.velocity = np.zeros(num_envs, dtype=np.float32)
19
+
20
+ def step(self, action) -> dict:
21
+ if np.isscalar(action):
22
+ action = np.full(self.num_envs, action, dtype=np.float32)
23
+ else:
24
+ action = np.asarray(action, dtype=np.float32)
25
+ if action.shape[0] != self.num_envs:
26
+ raise ValueError(
27
+ f"Expected actions of shape ({self.num_envs},), got {action.shape}"
28
+ )
29
+
30
+ force_term = (action - 1.0) * self.force
31
+ self.velocity += force_term + np.cos(3 * self.position) * (-self.gravity)
32
+ self.velocity = np.clip(self.velocity, -self.max_speed, self.max_speed)
33
+
34
+ self.position += self.velocity
35
+ self.position = np.clip(self.position, self.min_position, self.max_position)
36
+
37
+ # handle collision with left wall
38
+ wall_mask = (self.position == self.min_position) & (self.velocity < 0)
39
+ self.velocity[wall_mask] = 0.0
40
+ done = self.position >= self.goal_position
41
+
42
+ if self.num_envs == 1:
43
+ self.position = float(self.position[0])
44
+ self.velocity = float(self.velocity[0])
45
+ done = bool(done[0])
46
+
47
+ return {"position": self.position, "velocity": self.velocity, "done": done}
48
+
49
+ def reset(self) -> dict:
50
+ self.position = -0.5
51
+ self.velocity = 0.0
52
+ return {
53
+ "position": self.position,
54
+ "velocity": self.velocity,
55
+ "done": self.position >= self.goal_position,
56
+ }
@@ -0,0 +1,158 @@
1
+ export default {
2
+ async render({ model, el }) {
3
+ const [width, height] = model.get("_viewport_size") || [600, 400];
4
+ const container = document.createElement("div");
5
+ container.style.position = "relative";
6
+ el.appendChild(container);
7
+
8
+ const canvas = document.createElement("canvas");
9
+ canvas.width = width;
10
+ canvas.height = height;
11
+ container.appendChild(canvas);
12
+ const ctx = canvas.getContext("2d");
13
+
14
+ const sim_state = model.get("sim_state") || {};
15
+ const minPosition = -1.2;
16
+ const maxPosition = 0.6;
17
+ const maxSpeed = 0.07;
18
+ const force = 0.001;
19
+ const gravity = 0.0025;
20
+ let position = sim_state.position || -0.5;
21
+ let velocity = sim_state.velocity || 0.0;
22
+
23
+ if (model.get("_manual_control")) {
24
+ const controls = document.createElement("div");
25
+ controls.className = "controls";
26
+ controls.innerHTML = `
27
+ <button id="left">←</button>
28
+ <button id="right">→</button>
29
+ `;
30
+
31
+ const controlsObj = { action: 1 }; // 0=left, 1=idle, 2=right
32
+ container.appendChild(controls);
33
+
34
+ controls.addEventListener("mousedown", (e) => {
35
+ const id = e.target.id;
36
+ if (id === "left") {
37
+ controlsObj.action = 0;
38
+ } else if (id === "right") {
39
+ controlsObj.action = 2;
40
+ }
41
+ });
42
+
43
+ controls.addEventListener("mouseup", (e) => {
44
+ const id = e.target.id;
45
+ if (id === "left" || id === "right") {
46
+ controlsObj.action = 1;
47
+ }
48
+ });
49
+
50
+ let lastTime = new Date();
51
+ const step = () => {
52
+ let ms = new Date();
53
+ if (ms - lastTime < 20) {
54
+ requestAnimationFrame(step);
55
+ return;
56
+ }
57
+ lastTime = ms;
58
+
59
+ const action = controlsObj.action; // 0=left, 1=idle, 2=right
60
+ const forceTerm = (action - 1) * force;
61
+ velocity += forceTerm + Math.cos(3 * position) * -gravity;
62
+ velocity = Math.max(Math.min(velocity, maxSpeed), -maxSpeed);
63
+ position += velocity;
64
+ position = Math.max(Math.min(position, maxPosition), minPosition);
65
+ if (position === minPosition && velocity < 0) velocity = 0;
66
+ requestAnimationFrame(step);
67
+ };
68
+
69
+ step();
70
+ }
71
+
72
+ const goalPosition = 0.5;
73
+ const worldWidth = maxPosition - minPosition;
74
+ const scale = width / worldWidth;
75
+ const heightFn = (x) => Math.sin(3 * x) * 0.45 + 0.55;
76
+
77
+ const terrain = [];
78
+ for (let i = 0; i < 200; i++) {
79
+ const t = i / 199;
80
+ const xWorld = minPosition + t * worldWidth;
81
+ const yWorld = heightFn(xWorld);
82
+ terrain.push({
83
+ x: (xWorld - minPosition) * scale,
84
+ y: height - yWorld * scale
85
+ });
86
+ }
87
+
88
+ const carWidth = 40;
89
+ const carHeight = 20;
90
+ const clearance = 10;
91
+
92
+ model.on("change:sim_state", () => {
93
+ const newState = model.get("sim_state") || {};
94
+ position = newState.position;
95
+ velocity = newState.velocity;
96
+ });
97
+
98
+ const draw = () => {
99
+ ctx.clearRect(0, 0, width, height);
100
+ ctx.fillStyle = "#eeeeee";
101
+ ctx.beginPath();
102
+ ctx.moveTo(terrain[0].x, terrain[0].y);
103
+ for (const p of terrain) ctx.lineTo(p.x, p.y);
104
+ ctx.lineTo(width, height);
105
+ ctx.lineTo(0, height);
106
+ ctx.closePath();
107
+ ctx.fill();
108
+
109
+ ctx.strokeStyle = "#444";
110
+ ctx.lineWidth = 2;
111
+ ctx.stroke();
112
+ const xScreen = (position - minPosition) * scale;
113
+ const yScreen = height - heightFn(position) * scale - clearance;
114
+ const slope = Math.cos(3 * position);
115
+ const angle = Math.atan(slope);
116
+
117
+ ctx.save();
118
+ ctx.translate(xScreen, yScreen);
119
+ ctx.rotate(-angle);
120
+
121
+ ctx.fillStyle = "#000";
122
+ ctx.fillRect(-carWidth / 2, -carHeight, carWidth, carHeight);
123
+
124
+ ctx.fillStyle = "#777";
125
+ const wheelR = carHeight * 0.4;
126
+ ctx.beginPath();
127
+ ctx.arc(carWidth / 3, 0, wheelR, 0, Math.PI * 2);
128
+ ctx.fill();
129
+ ctx.beginPath();
130
+ ctx.arc(-carWidth / 3, 0, wheelR, 0, Math.PI * 2);
131
+ ctx.fill();
132
+ ctx.restore();
133
+
134
+ const fx = (goalPosition - minPosition) * scale;
135
+ const fy = height - heightFn(goalPosition) * scale;
136
+ ctx.strokeStyle = "#000";
137
+ ctx.beginPath();
138
+ ctx.moveTo(fx, fy);
139
+ ctx.lineTo(fx, fy - 40);
140
+ ctx.stroke();
141
+
142
+ ctx.fillStyle = "#ff0";
143
+ ctx.beginPath();
144
+ ctx.moveTo(fx, fy - 40);
145
+ ctx.lineTo(fx + 25, fy - 35);
146
+ ctx.lineTo(fx, fy - 30);
147
+ ctx.closePath();
148
+ ctx.fill();
149
+
150
+ requestAnimationFrame(draw);
151
+ };
152
+
153
+ draw();
154
+
155
+ model.set("_view_ready", true);
156
+ model.save_changes();
157
+ }
158
+ };
@@ -0,0 +1,5 @@
1
+ .controls {
2
+ display: flex;
3
+ margin-top: 10px;
4
+ gap: 10px;
5
+ }
@@ -0,0 +1,141 @@
1
+ import asyncio
2
+ import math
3
+ import numpy as np
4
+ from . import MountainCarEnv
5
+
6
+ try:
7
+ import tkinter as tk
8
+ from .. import _tk_base
9
+ except ImportError:
10
+ raise ImportError("tkinter is required for MountainCarTkFrontend")
11
+
12
+
13
+ class MountainCarTkFrontend(_tk_base.TkBaseFrontend):
14
+
15
+ def __init__(self, viewport_size=(600, 400), sim_env=None):
16
+ super().__init__()
17
+ if sim_env is None:
18
+ sim_env = MountainCarEnv()
19
+ self.sim_env = sim_env
20
+ self._viewport_size = viewport_size
21
+
22
+ async def step(self, action, dt=0.01):
23
+ state = self.sim_env.step(action)
24
+
25
+ if self._root:
26
+ self._root.after(0, lambda s=state: self._draw_state(s))
27
+
28
+ await asyncio.sleep(dt)
29
+ return state
30
+
31
+ async def reset(self):
32
+ state = self.sim_env.reset()
33
+ if self._canvas:
34
+ self._draw_state(state)
35
+ return state
36
+
37
+ def _create_window(self, root):
38
+ w, h = self._viewport_size
39
+ root.title("Mountain Car")
40
+ canvas = tk.Canvas(root, width=w, height=h, bg="#eeeeee")
41
+ canvas.pack(fill="both", expand=True)
42
+ self.bring_to_front(root)
43
+ self._root = root
44
+ self._canvas = canvas
45
+ self._draw_state(self.sim_env.reset())
46
+ self._pump()
47
+ root.mainloop()
48
+
49
+ def _draw_state(self, state: dict):
50
+ if not self._canvas:
51
+ return
52
+
53
+ c = self._canvas
54
+ w = int(c.winfo_width() or self._viewport_size[0])
55
+ h = int(c.winfo_height() or self._viewport_size[1])
56
+ c.delete("all")
57
+
58
+ min_x = self.sim_env.min_position
59
+ max_x = self.sim_env.max_position
60
+ world_width = max_x - min_x
61
+ scale = w / world_width
62
+ clearance = 10
63
+
64
+ def heightFn(x):
65
+ return np.sin(3 * x) * 0.45 + 0.55
66
+
67
+ # Terrain
68
+ terrain_pts = []
69
+ for px in range(w):
70
+ x_world = min_x + px / w * world_width
71
+ y_world = heightFn(x_world)
72
+ y_screen = h - y_world * scale
73
+ terrain_pts.append((px, y_screen))
74
+ for i in range(len(terrain_pts) - 1):
75
+ c.create_line(*terrain_pts[i], *terrain_pts[i + 1], fill="#444444", width=2)
76
+
77
+ # Goal
78
+ gx = self.sim_env.goal_position
79
+ gy = heightFn(gx)
80
+ goal_x = (gx - min_x) * scale
81
+ goal_y = h - gy * scale
82
+ c.create_line(goal_x, goal_y, goal_x, goal_y - 40, fill="#000", width=2)
83
+ c.create_polygon(
84
+ goal_x,
85
+ goal_y - 40,
86
+ goal_x + 25,
87
+ goal_y - 35,
88
+ goal_x,
89
+ goal_y - 30,
90
+ fill="#ffff00",
91
+ outline="",
92
+ )
93
+
94
+ # Draw each car
95
+ positions = np.atleast_1d(state["position"])
96
+ car_width, car_height = 40, 20
97
+ wheel_r = car_height * 0.4
98
+
99
+ def rot(px, py, ang):
100
+ s, c0 = math.sin(ang), math.cos(ang)
101
+ return px * c0 - py * s, px * s + py * c0
102
+
103
+ colors = ["#ff0000", "#00aa00", "#0000ff", "#ffaa00"]
104
+
105
+ for i, x_world in enumerate(positions):
106
+ y_world = heightFn(x_world)
107
+ x_screen = (x_world - min_x) * scale
108
+ y_screen = h - y_world * scale - clearance
109
+
110
+ slope = math.cos(3 * x_world)
111
+ angle = -math.atan(slope)
112
+
113
+ # body
114
+ body_local = [
115
+ (-car_width / 2, -car_height),
116
+ (car_width / 2, -car_height),
117
+ (car_width / 2, 0),
118
+ (-car_width / 2, 0),
119
+ ]
120
+ body_screen = []
121
+ for px, py in body_local:
122
+ rx, ry = rot(px, py, angle)
123
+ body_screen.append((x_screen + rx, y_screen + ry))
124
+
125
+ c.create_polygon(
126
+ body_screen, fill=colors[i % len(colors)], outline="", stipple="gray75"
127
+ )
128
+
129
+ # wheels
130
+ for wx in (-car_width / 3, car_width / 3):
131
+ rx, ry = rot(wx, 0, angle)
132
+ cx = x_screen + rx
133
+ cy = y_screen + ry
134
+ c.create_oval(
135
+ cx - wheel_r,
136
+ cy - wheel_r,
137
+ cx + wheel_r,
138
+ cy + wheel_r,
139
+ fill="#777777",
140
+ outline="",
141
+ )
@@ -0,0 +1,56 @@
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 MountainCarEnv
9
+
10
+
11
+ class MountainCarWidget(anywidget.AnyWidget):
12
+ _esm = pathlib.Path(__file__).parent / "sim.js"
13
+ _css = pathlib.Path(__file__).parent / "styles.css"
14
+
15
+ sim_state = traitlets.Dict(default_value={}).tag(sync=True)
16
+ _viewport_size = traitlets.Tuple(
17
+ traitlets.Int(), traitlets.Int(), default_value=(600, 400)
18
+ ).tag(sync=True)
19
+ _view_ready = traitlets.Bool(default_value=False).tag(sync=True)
20
+ _manual_control = traitlets.Bool(default_value=False).tag(sync=True)
21
+
22
+ def __init__(self, manual_control=False, viewport_size=(600, 400), sim_env=None):
23
+ self._manual_control = manual_control
24
+ self._viewport_size = viewport_size
25
+ super().__init__()
26
+ if sim_env is None:
27
+ sim_env = MountainCarEnv()
28
+
29
+ if sim_env.num_envs != 1:
30
+ raise ValueError(
31
+ "MountainCarWidget currently only supports single environment."
32
+ )
33
+
34
+ self.sim_env = sim_env
35
+ self.sim_state = self.sim_env.reset()
36
+
37
+ def render(self):
38
+ display(self)
39
+
40
+ try:
41
+ with ui_events() as ui_poll:
42
+ while not self._view_ready:
43
+ ui_poll(100)
44
+ except Exception:
45
+ pass
46
+
47
+ async def step(self, action: int, dt: float = 0.01) -> dict:
48
+ sim_state = self.sim_env.step(action)
49
+ self.sim_state = sim_state
50
+ await asyncio.sleep(dt)
51
+ return sim_state
52
+
53
+ async def reset(self) -> dict:
54
+ sim_state = self.sim_env.reset()
55
+ self.sim_state = sim_state
56
+ return sim_state
File without changes
@@ -0,0 +1,121 @@
1
+ import random
2
+ import math
3
+ import copy
4
+
5
+ empty_map = {
6
+ "name": "rect_triangle_map",
7
+ "robot": {
8
+ "pos": [500, 300],
9
+ "angle": 0,
10
+ }, # speed: 0.01, turn_speed: 0.04, lidar: { numBeams: 30, fov: 6.28, maxRange: 1000}
11
+ "map": [
12
+ {
13
+ "type": "rectangle",
14
+ "x": 400,
15
+ "y": 0,
16
+ "width": 800,
17
+ "height": 30,
18
+ "bodyInfo": {"angle": 0, "render": {"fillStyle": "#2f6f8f"}},
19
+ "label": "Wall",
20
+ },
21
+ {
22
+ "type": "rectangle",
23
+ "x": 400,
24
+ "y": 590,
25
+ "width": 800,
26
+ "height": 20,
27
+ "bodyInfo": {"angle": 0, "render": {"fillStyle": "#2f6f8f"}},
28
+ "label": "Wall",
29
+ },
30
+ {
31
+ "type": "rectangle",
32
+ "x": 10,
33
+ "y": 500,
34
+ "width": 20,
35
+ "height": 1000,
36
+ "bodyInfo": {"angle": 0, "render": {"fillStyle": "#2f6f8f"}},
37
+ "label": "Wall",
38
+ },
39
+ {
40
+ "type": "rectangle",
41
+ "x": 790,
42
+ "y": 500,
43
+ "width": 20,
44
+ "height": 1000,
45
+ "bodyInfo": {"angle": 0, "render": {"fillStyle": "#2f6f8f"}},
46
+ "label": "Wall",
47
+ },
48
+ ],
49
+ }
50
+
51
+
52
+ def gen_simple_map():
53
+ map_data = copy.deepcopy(empty_map)
54
+ width = 800
55
+ height = 600
56
+
57
+ # Generate static obstacles
58
+ for _ in range(6):
59
+ w = 60 + random.random() * 120
60
+ h = 40 + random.random() * 100
61
+ x = 120 + random.random() * (width - 240)
62
+ y = 120 + random.random() * (height - 240)
63
+ angle = random.random() * math.pi
64
+ rect = {
65
+ "type": "rectangle",
66
+ "x": x,
67
+ "y": y,
68
+ "width": w,
69
+ "height": h,
70
+ "bodyInfo": {
71
+ "angle": angle,
72
+ "render": {"fillStyle": "#2f6f8f"},
73
+ "label": "Obstacle",
74
+ },
75
+ }
76
+ map_data["map"].append(rect)
77
+
78
+ # Generate dynamic pushable boxes
79
+ for _ in range(3):
80
+ size = 40 + random.random() * 20
81
+ x = 100 + random.random() * (width - 200)
82
+ y = 100 + random.random() * (height - 200)
83
+ box = {
84
+ "type": "rectangle",
85
+ "x": x,
86
+ "y": y,
87
+ "width": size,
88
+ "height": size,
89
+ "bodyInfo": {
90
+ "isStatic": False,
91
+ "frictionAir": 0.1,
92
+ "friction": 0.5,
93
+ "restitution": 0.2,
94
+ "density": 0.002,
95
+ "render": {"fillStyle": "#b2df8a"},
96
+ "label": "Pushable Box",
97
+ },
98
+ }
99
+ map_data["map"].append(box)
100
+
101
+ for i in range(2):
102
+ size = 40 + random.random() * 20
103
+ x = 100 + random.random() * (width - 200)
104
+ y = 100 + random.random() * (height - 200)
105
+ angle = random.random() * math.pi
106
+ box = {
107
+ "type": "rectangle",
108
+ "x": x,
109
+ "y": y,
110
+ "width": size,
111
+ "height": size,
112
+ "bodyInfo": {
113
+ "isStatic": True,
114
+ "angle": angle,
115
+ "label": "Goal",
116
+ "render": {"fillStyle": "#ffffff"},
117
+ },
118
+ }
119
+ map_data["map"].append(box)
120
+
121
+ return map_data