Spaces:
Running
Running
| """ | |
| InteractiveObjects — fyzické předměty pro haptické a kognitivní úlohy. | |
| BallObject – gumový míč s vysokou elasticitou | |
| WaterCupObject – hrnek s hladinou vody (kloní se = přelévá) | |
| RubiksCubeObject – zjednodušená Rubikova kostka (fyzické těleso + logický stav) | |
| ShapeSorterWall – zeď s otvory (čtverec, trojúhelník, hvězda) + odpovídající tvar | |
| """ | |
| from __future__ import annotations | |
| import sys, os | |
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) | |
| import numpy as np | |
| from dataclasses import dataclass, field | |
| from typing import Dict, List, Optional, Tuple | |
| from engine.physics.rigid_body import ( | |
| PhysicsWorld, RigidBody, BodyType, | |
| vec3, Vec3, length, normalize, | |
| ) | |
| # ── Základní třída ──────────────────────────────────────────────────────────── | |
| class PhysicsObject: | |
| """Základní třída pro interaktivní objekty.""" | |
| def __init__(self, phys: PhysicsWorld, obj_id: str): | |
| self.phys = phys | |
| self.obj_id = obj_id | |
| self.body: Optional[RigidBody] = None | |
| def position(self) -> Vec3: | |
| return self.body.position if self.body else vec3() | |
| def velocity(self) -> Vec3: | |
| return self.body.velocity if self.body else vec3() | |
| def distance_to(self, pos: Vec3) -> float: | |
| return float(length(self.position - pos)) | |
| # ── Míč ─────────────────────────────────────────────────────────────────────── | |
| class BallObject(PhysicsObject): | |
| """ | |
| Gumový míč — malé těleso s vysokou elasticitou a nízkou hmotností. | |
| Odraz: restitution=0.82 (≈ gumový míč, ne super-ball). | |
| """ | |
| RADIUS = 0.12 # m | |
| MASS = 0.25 # kg | |
| def __init__(self, phys: PhysicsWorld, pos: Vec3, obj_id: str = "ball"): | |
| super().__init__(phys, obj_id) | |
| self.body = phys.create_body( | |
| obj_id, BodyType.DYNAMIC, | |
| position=pos, | |
| size=vec3(self.RADIUS * 2, self.RADIUS * 2, self.RADIUS * 2), | |
| mass=self.MASS, | |
| restitution=0.82, | |
| friction=0.35, | |
| ) | |
| self._spawn_pos = pos | |
| def reset(self, pos: Optional[Vec3] = None): | |
| p = pos if pos is not None else self._spawn_pos | |
| self.body.position = p | |
| self.body.velocity = vec3() | |
| self.body.angular_velocity = vec3() | |
| self.body.rotation = vec3() | |
| def get_state(self) -> np.ndarray: | |
| """6 floats: [pos_x, pos_y, pos_z, vel_x, vel_y, vel_z]""" | |
| p = self.body.position | |
| v = self.body.velocity | |
| return np.array([p[0], p[1], p[2], v[0], v[1], v[2]], dtype=np.float32) | |
| # ── Hrnek s vodou ───────────────────────────────────────────────────────────── | |
| class WaterCupObject(PhysicsObject): | |
| """ | |
| Keramický hrnek s hladinou vody. | |
| water_level klesá, když je hrnek nakloněný (tilt > threshold). | |
| Použití: agent se musí naučit nosit hrnek opatrně. | |
| """ | |
| MASS = 0.35 | |
| SIZE = vec3(0.09, 0.12, 0.09) | |
| TILT_SPILL_THRESH = 0.40 # rad od svislé osy | |
| SPILL_RATE = 0.03 # úbytek hladiny za krok při náklonu | |
| def __init__(self, phys: PhysicsWorld, pos: Vec3, obj_id: str = "water_cup"): | |
| super().__init__(phys, obj_id) | |
| self.body = phys.create_body( | |
| obj_id, BodyType.DYNAMIC, | |
| position=pos, | |
| size=self.SIZE, | |
| mass=self.MASS, | |
| restitution=0.08, | |
| friction=0.70, | |
| ) | |
| self._spawn_pos = pos | |
| self.water_level = 1.0 # 0.0 = prázdný, 1.0 = plný | |
| self._spilled = False | |
| def step(self): | |
| """Volat každý fyzikální krok.""" | |
| if self.water_level <= 0.0: | |
| return | |
| rot = self.body.rotation | |
| tilt = float(np.sqrt(rot[0] ** 2 + rot[2] ** 2)) | |
| if tilt > self.TILT_SPILL_THRESH: | |
| self.water_level = max(0.0, self.water_level - self.SPILL_RATE) | |
| if self.water_level == 0.0: | |
| self._spilled = True | |
| def reset(self, pos: Optional[Vec3] = None): | |
| p = pos if pos is not None else self._spawn_pos | |
| self.body.position = p | |
| self.body.velocity = vec3() | |
| self.body.angular_velocity = vec3() | |
| self.body.rotation = vec3() | |
| self.water_level = 1.0 | |
| self._spilled = False | |
| def get_state(self) -> np.ndarray: | |
| """4 floats: [pos_y_rel_spawn, tilt, water_level, spilled]""" | |
| rot = self.body.rotation | |
| tilt = float(np.sqrt(float(rot[0]) ** 2 + float(rot[2]) ** 2)) | |
| pos_y = float(self.body.position[1]) | |
| return np.array([pos_y, tilt, self.water_level, float(self._spilled)], | |
| dtype=np.float32) | |
| # ── Rubikova kostka ─────────────────────────────────────────────────────────── | |
| # Každá stěna má 9 políček → 6 stěn × 9 = 54 hodnot (0-5 = 6 barev) | |
| # Zjednodušení: jedna fyzická krychle, logický stav jako int array | |
| _SOLVED_FACES = np.array([ | |
| [i] * 9 for i in range(6) # každá stěna má jednu barvu | |
| ], dtype=np.int8).flatten() | |
| class RubiksCubeObject(PhysicsObject): | |
| """ | |
| Fyzická krychle s logickým stavem 6×9 políček (6 barev). | |
| Rotace stěn: rotate_face(face_idx, clockwise). | |
| Stav: stupeň zamíchanosti (0=vyřešeno, 1=přeskupeno). | |
| """ | |
| SIZE = vec3(0.12, 0.12, 0.12) | |
| MASS = 0.15 | |
| # Indexy stěn | |
| FRONT, BACK, LEFT, RIGHT, TOP, BOTTOM = 0, 1, 2, 3, 4, 5 | |
| # Otáčení: stěna 0-5 → seznam indexů políček (zjednodušeno — ring rotation) | |
| _FACE_RING = { | |
| 0: ([6,7,8],[0,3,6],[2,1,0],[8,5,2], [BACK:=1,LEFT:=2,TOP:=4,RIGHT:=3]), | |
| } | |
| def __init__(self, phys: PhysicsWorld, pos: Vec3, obj_id: str = "rubiks"): | |
| super().__init__(phys, obj_id) | |
| self.body = phys.create_body( | |
| obj_id, BodyType.DYNAMIC, | |
| position=pos, | |
| size=self.SIZE, | |
| mass=self.MASS, | |
| restitution=0.10, | |
| friction=0.80, | |
| ) | |
| self._spawn_pos = pos | |
| self.faces = _SOLVED_FACES.copy() # 54 políček | |
| self._moves = 0 | |
| def scramble(self, n_moves: int = 20, rng: np.random.Generator = None): | |
| if rng is None: | |
| rng = np.random.default_rng() | |
| for _ in range(n_moves): | |
| face = int(rng.integers(0, 6)) | |
| self._rotate_face_logical(face, bool(rng.integers(0, 2))) | |
| self._moves = 0 | |
| def _rotate_face_logical(self, face: int, clockwise: bool): | |
| """Otočí políčka na jedné stěně o 90°.""" | |
| f = self.faces[face * 9:(face + 1) * 9].copy().reshape(3, 3) | |
| if clockwise: | |
| f = np.rot90(f, k=3) | |
| else: | |
| f = np.rot90(f, k=1) | |
| self.faces[face * 9:(face + 1) * 9] = f.flatten() | |
| self._moves += 1 | |
| def is_solved(self) -> bool: | |
| return np.array_equal(self.faces, _SOLVED_FACES) | |
| def solve_fraction(self) -> float: | |
| """Poměr správně umístěných políček (0.0–1.0).""" | |
| return float(np.mean(self.faces == _SOLVED_FACES)) | |
| def reset(self, pos: Optional[Vec3] = None, scramble_n: int = 0, | |
| rng: np.random.Generator = None): | |
| p = pos if pos is not None else self._spawn_pos | |
| self.body.position = p | |
| self.body.velocity = vec3() | |
| self.body.angular_velocity = vec3() | |
| self.body.rotation = vec3() | |
| self.faces = _SOLVED_FACES.copy() | |
| self._moves = 0 | |
| if scramble_n > 0: | |
| self.scramble(scramble_n, rng) | |
| def get_state(self) -> np.ndarray: | |
| """3 floats: [solve_fraction, n_moves_norm, is_held_approx(pos_y)]""" | |
| return np.array([ | |
| self.solve_fraction(), | |
| min(1.0, self._moves / 100.0), | |
| float(np.clip((float(self.body.position[1]) - 0.5) / 1.5, 0.0, 1.0)), | |
| ], dtype=np.float32) | |
| # ── Třídicí zeď ─────────────────────────────────────────────────────────────── | |
| SHAPE_SQUARE = 0 | |
| SHAPE_TRIANGLE = 1 | |
| SHAPE_STAR = 2 | |
| # Fyzické rozměry tvarových předmětů | |
| _SHAPE_SIZES = { | |
| SHAPE_SQUARE: vec3(0.09, 0.09, 0.04), | |
| SHAPE_TRIANGLE: vec3(0.09, 0.09, 0.04), | |
| SHAPE_STAR: vec3(0.10, 0.10, 0.04), | |
| } | |
| # Barvy tvarů (R, G, B) pro renderer | |
| SHAPE_COLORS = { | |
| SHAPE_SQUARE: (0.9, 0.2, 0.2), # červená | |
| SHAPE_TRIANGLE: (0.2, 0.8, 0.2), # zelená | |
| SHAPE_STAR: (0.9, 0.8, 0.1), # žlutá | |
| } | |
| # Pozice otvorů (x-offset od středu zdi) | |
| _HOLE_X_OFFSETS = { | |
| SHAPE_SQUARE: -0.35, | |
| SHAPE_TRIANGLE: 0.00, | |
| SHAPE_STAR: 0.35, | |
| } | |
| # Tolerance vložení (vzdálenost + orientace) | |
| _INSERT_DIST_OK = 0.15 # m — střed tvaru musí být do 15 cm od otvoru | |
| _INSERT_TILT_OK = 0.30 # rad — maximální náklon tvaru při vkládání | |
| class ShapePiece: | |
| shape: int # SHAPE_* konstanta | |
| body: RigidBody | |
| inserted: bool = False | |
| spawn_pos: Vec3 = field(default_factory=lambda: vec3()) | |
| class ShapeSorterWall: | |
| """ | |
| Statická zeď s třemi otvory (čtverec, trojúhelník, hvězda). | |
| Tvarové předměty jsou dynamické tělesa, agent je musí vložit do správného otvoru. | |
| Kontrola vložení: vzdálenost od otvoru < 15 cm + přibližně správná orientace. | |
| """ | |
| WALL_POS = vec3(0.0, 1.0, -2.0) # zeď je v záporném Z (před agentem) | |
| WALL_SIZE = vec3(1.2, 1.0, 0.08) | |
| HOLE_Y = 1.0 # výška středu otvorů (v absolutních souřadnicích) | |
| def __init__(self, phys: PhysicsWorld, origin: Vec3 = None, rng=None): | |
| self.phys = phys | |
| self._rng = rng if rng is not None else np.random.default_rng() | |
| self.origin = origin if origin is not None else vec3() | |
| self._wall_body: Optional[RigidBody] = None | |
| self.pieces: List[ShapePiece] = [] | |
| self._spawn() | |
| def _spawn(self): | |
| wall_pos = self.origin + self.WALL_POS | |
| self._wall_body = self.phys.create_body( | |
| "shape_wall", BodyType.STATIC, | |
| position=wall_pos, | |
| size=self.WALL_SIZE, | |
| mass=0.0, restitution=0.05, friction=0.90, | |
| ) | |
| # Spawn tvarové předměty před zdi | |
| spawn_z = float(self.WALL_POS[2]) + 1.2 # 1.2 m před zdi | |
| shapes = [SHAPE_SQUARE, SHAPE_TRIANGLE, SHAPE_STAR] | |
| # Zamíchej pořadí spawnu | |
| order = self._rng.permutation(len(shapes)) | |
| x_slots = [-0.35, 0.0, 0.35] | |
| for i, shape_idx in enumerate(order): | |
| shape = shapes[shape_idx] | |
| pos = self.origin + vec3( | |
| x_slots[i], | |
| 0.80, # výška stolu/podlahy | |
| spawn_z, | |
| ) | |
| body = self.phys.create_body( | |
| f"shape_{shape}_{i}", BodyType.DYNAMIC, | |
| position=pos, | |
| size=_SHAPE_SIZES[shape], | |
| mass=0.08, restitution=0.10, friction=0.75, | |
| ) | |
| self.pieces.append(ShapePiece(shape=shape, body=body, spawn_pos=pos.copy())) | |
| def hole_position(self, shape: int) -> Vec3: | |
| """Absolutní pozice středu otvoru pro daný tvar.""" | |
| wx = float(self.WALL_POS[0]) + float(self.origin[0]) | |
| wz = float(self.WALL_POS[2]) + float(self.origin[2]) + 0.05 | |
| return vec3(wx + _HOLE_X_OFFSETS[shape], self.HOLE_Y, wz) | |
| def check_insertions(self) -> int: | |
| """Zkontroluje vložení předmětů; vrací počet správně vložených.""" | |
| count = 0 | |
| for piece in self.pieces: | |
| if piece.inserted: | |
| count += 1 | |
| continue | |
| hole = self.hole_position(piece.shape) | |
| dist = float(length(piece.body.position - hole)) | |
| rot = piece.body.rotation | |
| tilt = float(np.sqrt(float(rot[0]) ** 2 + float(rot[1]) ** 2)) | |
| if dist < _INSERT_DIST_OK and tilt < _INSERT_TILT_OK: | |
| piece.inserted = True | |
| # Zafixuj předmět na místě | |
| piece.body.body_type = BodyType.STATIC | |
| piece.body.velocity = vec3() | |
| count += 1 | |
| return count | |
| def all_inserted(self) -> bool: | |
| return all(p.inserted for p in self.pieces) | |
| def nearest_piece(self, hand_pos: Vec3) -> Optional[ShapePiece]: | |
| """Vrátí nejbližší nevložený předmět k ruce agenta.""" | |
| best_d = float("inf") | |
| best_p = None | |
| for p in self.pieces: | |
| if p.inserted: | |
| continue | |
| d = float(length(p.body.position - hand_pos)) | |
| if d < best_d: | |
| best_d, best_p = d, p | |
| return best_p | |
| def reset(self): | |
| for piece in self.pieces: | |
| piece.inserted = False | |
| piece.body.body_type = BodyType.DYNAMIC | |
| piece.body.position = piece.spawn_pos.copy() | |
| piece.body.velocity = vec3() | |
| piece.body.angular_velocity = vec3() | |
| piece.body.rotation = vec3() | |
| def get_state(self) -> np.ndarray: | |
| """ | |
| 9 floats: pro každý předmět (3×): | |
| [dist_to_hole_norm, inserted, piece_pos_y_norm] | |
| """ | |
| out = [] | |
| for piece in self.pieces: | |
| hole = self.hole_position(piece.shape) | |
| dist = float(length(piece.body.position - hole)) | |
| out.extend([ | |
| float(np.clip(dist / 3.0, 0.0, 1.0)), | |
| float(piece.inserted), | |
| float(np.clip(float(piece.body.position[1]) / 2.0, 0.0, 1.0)), | |
| ]) | |
| return np.array(out, dtype=np.float32) | |