# NumPy Tutorial: Dal Tuo Sistema di Collisioni al Codice Ottimizzato Questo documento spiega NumPy usando come esempio reale il sistema di collisioni di Mice!, confrontando il tuo approccio originale con la versione ottimizzata. ## Indice 1. [Introduzione: Il Problema delle Performance](#1-introduzione-il-problema-delle-performance) 2. [Cos'è NumPy e Perché Serve](#2-cosè-numpy-e-perché-serve) 3. [Concetti Base di NumPy](#3-concetti-base-di-numpy) 4. [Dal Tuo Codice a NumPy: Caso Pratico](#4-dal-tuo-codice-a-numpy-caso-pratico) 5. [Spatial Hashing: L'Algoritmo Intelligente](#5-spatial-hashing-lalgoritmo-intelligente) 6. [Operazioni Vettoriali in NumPy](#6-operazioni-vettoriali-in-numpy) 7. [Best Practices e Pitfalls](#7-best-practices-e-pitfalls) --- ## 1. Introduzione: Il Problema delle Performance ### Il Tuo Sistema Originale (Funzionava Bene!) ```python # rats.py - Il tuo approccio originale def update_maze(self): # Popolava dizionari con le posizioni delle unità self.unit_positions = {} self.unit_positions_before = {} for unit in self.units.values(): unit.move() # Raggruppa unità per posizione self.unit_positions.setdefault(unit.position, []).append(unit) self.unit_positions_before.setdefault(unit.position_before, []).append(unit) for unit in self.units.values(): unit.collisions() # Ogni unità controlla le proprie collisioni ``` ### Il Problema con 200+ Unità Con 5-10 topi: **funziona perfetto** ✅ Con 200+ topi: **FPS crollano** ❌ **Perché?** - Ogni topo controlla collisioni con TUTTI gli altri topi - 200 topi = 200 × 200 = **40,000 controlli per frame!** - Complessità: **O(n²)** - cresce in modo quadratico --- ## 2. Cos'è NumPy e Perché Serve ### NumPy in 3 Parole **Array multidimensionali ottimizzati** ### Perché è Veloce? ```python # Python puro (lento ❌) distances = [] for i in range(1000): for j in range(1000): dx = x[i] - y[j] dy = x[i] - y[j] distances.append((dx**2 + dy**2)**0.5) # Tempo: ~500ms con 1 milione di operazioni # NumPy (veloce ✅) import numpy as np distances = np.sqrt((x[:, None] - y[None, :])**2 + (x[:, None] - y[None, :])**2) # Tempo: ~5ms - 100 volte più veloce! ``` ### Perché la Differenza? 1. **Codice C Compilato**: NumPy è scritto in C/C++, non Python interpretato 2. **Operazioni Vettoriali**: Calcola migliaia di valori in parallelo 3. **Memoria Contigua**: Dati organizzati efficientemente in RAM 4. **CPU SIMD**: Usa istruzioni speciali della CPU per parallelismo hardware --- ## 3. Concetti Base di NumPy ### Array vs Liste Python ```python # Lista Python (flessibile ma lenta) lista = [1, 2, 3, 4, 5] lista.append("sei") # OK - tipi misti lista[0] = "uno" # OK - cambio tipo # Array NumPy (veloce ma rigido) import numpy as np array = np.array([1, 2, 3, 4, 5]) # array[0] = "uno" # ERRORE! Tipo fisso: int64 ``` **Regola**: NumPy sacrifica flessibilità per velocità ### Operazioni Elemento per Elemento ```python # Python puro lista_a = [1, 2, 3] lista_b = [4, 5, 6] risultato = [] for a, b in zip(lista_a, lista_b): risultato.append(a + b) # risultato = [5, 7, 9] # NumPy (broadcasting) array_a = np.array([1, 2, 3]) array_b = np.array([4, 5, 6]) risultato = array_a + array_b # [5, 7, 9] - automatico! ``` ### Broadcasting: Operazioni su Array di Dimensioni Diverse ```python # Esempio reale dal tuo gioco: calcolare distanze unit_positions = np.array([[10, 20], [30, 40], [50, 60]]) # 3 unità target = np.array([25, 35]) # 1 bersaglio # Vogliamo: distanza di ogni unità dal bersaglio # Senza broadcasting (noioso): distances = [] for pos in unit_positions: dx = pos[0] - target[0] dy = pos[1] - target[1] distances.append(np.sqrt(dx**2 + dy**2)) # Con broadcasting (elegante): diff = unit_positions - target # NumPy espande target automaticamente distances = np.sqrt((diff**2).sum(axis=1)) # Output: [18.03, 7.07, 28.28] ``` **Come funziona?** ``` unit_positions: [[10, 20], target: [25, 35] [30, 40], [50, 60]] Broadcasting lo espande a: [[25, 35], [25, 35], [25, 35]] ``` --- ## 4. Dal Tuo Codice a NumPy: Caso Pratico ### Fase 1: Il Tuo Approccio con i Dizionari ```python # units/rat.py - Il tuo codice originale def collisions(self): # Prende unità nella stessa cella units_here = self.game.unit_positions.get(self.position, []) units_before = self.game.unit_positions_before.get(self.position_before, []) # Controlla ogni unità for other_unit in units_here + units_before: if other_unit.id == self.id: continue # Logica di collisione... if self.sex == other_unit.sex and self.fight: self.die(other_unit) elif self.sex != other_unit.sex: self.fuck(other_unit) ``` **Pro del Tuo Approccio:** - ✅ Semplice e leggibile - ✅ Usa dizionari Python nativi - ✅ Funziona perfettamente con poche unità **Problema con 200+ Unità:** - ❌ Ogni topo itera su liste di Python - ❌ Controlli ripetuti (topo A controlla B, poi B controlla A) - ❌ Nessuna ottimizzazione per distanze ### Fase 2: Spatial Hashing (L'Idea Geniale) Prima di NumPy, serve un algoritmo migliore: **Spatial Hashing** ```python # Concetto: dividi il mondo in "celle" (griglia) # Ogni cella contiene solo le unità al suo interno # Mondo di gioco: # 0 1 2 3 # 0 [ ] [ ] [ ] [ ] # 1 [ ] [A] [B] [ ] # 2 [ ] [ ] [C] [ ] # 3 [ ] [ ] [ ] [ ] # Dizionario spatial hash: spatial_grid = { (1, 1): [unit_A], (2, 1): [unit_B], (2, 2): [unit_C] } # Quando unit_A cerca collisioni: # Controlla SOLO celle (1,1) e adiacenti (0,0), (0,1), (0,2), (1,0), (1,2), (2,0), (2,1), (2,2) # Non controlla unit_C a (2,2) - troppo lontano! ``` **Vantaggio**: Da O(n²) a O(n)! - 200 unità: da 40,000 controlli a ~1,800 controlli (celle adiacenti) ### Fase 3: NumPy per Calcoli Massivi ```python # engine/collision_system.py - Il nuovo approccio class CollisionSystem: def __init__(self, cell_size=32): # Pre-allocazione: prepara spazio per array NumPy self.unit_ids = np.zeros(100, dtype=np.int64) # Array di ID self.bboxes = np.zeros((100, 4), dtype=np.float32) # Array di bounding box self.positions = np.zeros((100, 2), dtype=np.int32) # Array di posizioni self.current_size = 0 # Quante unità registrate self.capacity = 100 # Capacità massima prima di resize ``` **Perché Pre-allocazione?** ```python # Cattivo: crescita lenta ❌ array = np.array([]) for i in range(1000): array = np.append(array, i) # Crea NUOVO array ogni volta! # Tempo: ~200ms # Buono: pre-allocazione ✅ array = np.zeros(1000) for i in range(1000): array[i] = i # Modifica array esistente # Tempo: ~2ms ``` ### Fase 4: Registrazione Unità ```python def register_unit(self, unit_id, bbox, position, position_before, collision_layer): """Registra un'unità nel sistema di collisione""" # Se array pieno, raddoppia capacità if self.current_size >= self.capacity: self._resize_arrays(self.capacity * 2) idx = self.current_size # Inserisci dati negli array NumPy self.unit_ids[idx] = unit_id self.bboxes[idx] = bbox # [x1, y1, x2, y2] self.positions[idx] = position self.position_before[idx] = position_before self.layers[idx] = collision_layer.value # Spatial hashing: aggiungi a griglia cell = (position[0], position[1]) self.spatial_grid[cell].append(idx) # Salva INDICE, non unità self.current_size += 1 ``` **Nota Importante**: Salviamo **indici** negli array, non oggetti Python! - `spatial_grid[(5, 10)] = [0, 3, 7]` → Unità agli indici 0, 3, 7 degli array NumPy - Accesso veloce: `self.bboxes[0]`, `self.bboxes[3]`, `self.bboxes[7]` ### Fase 5: Collisioni Vettoriali con NumPy ```python def get_collisions_for_unit(self, unit_id, bbox, collision_layer): """Trova tutte le collisioni per un'unità""" # 1. Trova celle da controllare (spatial hashing) x, y = bbox[0] // self.cell_size, bbox[1] // self.cell_size cells_to_check = [ (x-1, y-1), (x, y-1), (x+1, y-1), (x-1, y), (x, y), (x+1, y), (x-1, y+1), (x, y+1), (x+1, y+1) ] # 2. Raccogli candidati da celle adiacenti candidates = [] for cell in cells_to_check: candidates.extend(self.spatial_grid.get(cell, [])) if len(candidates) < 10: # POCHI candidati: usa Python normale collisions = [] for idx in candidates: if self.unit_ids[idx] == unit_id: continue if self._check_bbox_collision(bbox, self.bboxes[idx]): collisions.append((self.layers[idx], self.unit_ids[idx])) return collisions else: # MOLTI candidati: USA NUMPY! ✨ return self._vectorized_collision_check(unit_id, bbox, candidates) ``` ### Fase 6: La Magia di NumPy - Vectorized Collision Check ```python def _vectorized_collision_check(self, unit_id, bbox, candidate_indices): """Controlla collisioni usando NumPy per massima velocità""" # Converti candidati in array NumPy candidate_indices = np.array(candidate_indices, dtype=np.int32) # Filtra l'unità stessa (non collidere con se stessi) mask = self.unit_ids[candidate_indices] != unit_id candidate_indices = candidate_indices[mask] if len(candidate_indices) == 0: return [] # Estrai bounding box di TUTTI i candidati in un colpo solo candidate_bboxes = self.bboxes[candidate_indices] # Shape: (N, 4) # candidate_bboxes = [[x1, y1, x2, y2], # candidato 0 # [x1, y1, x2, y2], # candidato 1 # ...] # Controllo collisione AABB (Axis-Aligned Bounding Box) # Due rettangoli collidono se: # - bbox.x1 < other.x2 AND # - bbox.x2 > other.x1 AND # - bbox.y1 < other.y2 AND # - bbox.y2 > other.y1 # NumPy calcola TUTTE le collisioni contemporaneamente! 🚀 colliding_mask = ( (bbox[0] < candidate_bboxes[:, 2]) & # bbox.x1 < others.x2 (bbox[2] > candidate_bboxes[:, 0]) & # bbox.x2 > others.x1 (bbox[1] < candidate_bboxes[:, 3]) & # bbox.y1 < others.y2 (bbox[3] > candidate_bboxes[:, 1]) # bbox.y2 > others.y1 ) # colliding_mask = [True, False, True, False, True, ...] # Filtra solo unità che collidono colliding_indices = candidate_indices[colliding_mask] # Restituisci coppie (layer, unit_id) return list(zip( self.layers[colliding_indices], self.unit_ids[colliding_indices] )) ``` **Spiegazione Dettagliata del Codice NumPy:** ```python # Esempio concreto con 3 candidati bbox = [10, 20, 30, 40] # Nostro topo: x1=10, y1=20, x2=30, y2=40 candidate_bboxes = np.array([ [5, 15, 25, 35], # Candidato 0 [50, 60, 70, 80], # Candidato 1 (lontano) [15, 25, 35, 45] # Candidato 2 ]) # Controllo bbox[0] < candidate_bboxes[:, 2] # bbox[0] = 10 # candidate_bboxes[:, 2] = [25, 70, 35] # Colonna x2 di tutti i candidati # 10 < [25, 70, 35] = [True, True, True] # Controllo bbox[2] > candidate_bboxes[:, 0] # bbox[2] = 30 # candidate_bboxes[:, 0] = [5, 50, 15] # Colonna x1 # 30 > [5, 50, 15] = [True, False, True] # ... altri controlli ... # Combinazione finale (AND logico): colliding_mask = [True, False, True] # Solo 0 e 2 collidono! ``` --- ## 5. Spatial Hashing: L'Algoritmo Intelligente ### Visualizzazione Pratica ``` Mondo di gioco 640x480, cell_size=32 Griglia spaziale: 0 1 2 3 4 5 ... 19 ┌────┬────┬────┬────┬────┬────┬────┬────┐ 0 │ │ │ │ │ │ │ │ │ ├────┼────┼────┼────┼────┼────┼────┼────┤ 1 │ │ R1 │ R2 │ │ │ │ │ │ R = Rat ├────┼────┼────┼────┼────┼────┼────┼────┤ B = Bomb 2 │ │ R3 │ B1 │ R4 │ │ │ │ │ M = Mine ├────┼────┼────┼────┼────┼────┼────┼────┤ 3 │ │ │ M1 │ │ │ │ │ │ └────┴────┴────┴────┴────┴────┴────┴────┘ ``` ### Come Funziona il Lookup ```python # R1 cerca collisioni da cella (1, 1) def get_collisions_for_unit(self, unit_id, bbox): x, y = 1, 1 # Posizione R1 # Controlla 9 celle (3x3 centrato su R1): cells = [ (0,0), (1,0), (2,0), # Riga sopra (0,1), (1,1), (2,1), # Riga centrale (include R1) (0,2), (1,2), (2,2) # Riga sotto ] # spatial_grid è un dizionario: # { # (1, 1): [idx_R1], # (2, 1): [idx_R2], # (1, 2): [idx_R3], # (2, 2): [idx_B1, idx_R4], # (2, 3): [idx_M1] # } candidates = [] for cell in cells: candidates.extend(self.spatial_grid.get(cell, [])) # candidates = [idx_R1, idx_R2, idx_R3, idx_B1, idx_R4] # NON include idx_M1 perché (2,3) è fuori dal range 3x3! ``` ### Benefici Misurabili ```python # SENZA spatial hashing (O(n²)): # 200 unità → 200 × 200 = 40,000 controlli # CON spatial hashing (O(n)): # 200 unità, distribuite su 20×15=300 celle # Media 0.67 unità per cella # Ogni unità controlla 9 celle × 0.67 = ~6 candidati # 200 unità × 6 candidati = 1,200 controlli # # Miglioramento: 40,000 → 1,200 = 33x più veloce! 🚀 ``` --- ## 6. Operazioni Vettoriali in NumPy ### Broadcasting Avanzato: Esplosioni ```python def get_units_in_area(self, positions, layer_filter=None): """Trova unità in un'area (es. esplosione bomba)""" # positions: lista di posizioni esplose # es. [(10, 10), (10, 11), (11, 10), (11, 11)] # Esplosione 2x2 if self.current_size == 0: return [] # Converti in array NumPy area_positions = np.array(positions, dtype=np.int32) # Shape: (4, 2) # Prendi posizioni di TUTTE le unità all_positions = self.positions[:self.current_size] # Shape: (N, 2) # es. all_positions = [[5, 5], [10, 10], [15, 15], [10, 11], ...] # Broadcasting trick per confrontare OGNI posizione esplosione con OGNI unità # area_positions[:, None, :] → Shape: (4, 1, 2) # all_positions[None, :, :] → Shape: (1, N, 2) # Risultato → Shape: (4, N, 2) - tutte le combinazioni! matches = (area_positions[:, None, :] == all_positions[None, :, :]).all(axis=2) # matches[i, j] = True se esplosione i colpisce unità j # any(axis=0): almeno una posizione esplosione colpisce quella unità? unit_hit_mask = matches.any(axis=0) # Shape: (N,) # Filtra per layer se richiesto if layer_filter: valid_layers = self.layers[:self.current_size] == layer_filter.value unit_hit_mask = unit_hit_mask & valid_layers # Restituisci ID delle unità colpite hit_indices = np.where(unit_hit_mask)[0] return self.unit_ids[hit_indices].tolist() ``` **Spiegazione con Esempio Concreto:** ```python # Bomba esplode creando 4 celle di fuoco area_positions = np.array([[10, 10], [10, 11], [11, 10], [11, 11]]) # Ci sono 3 topi nel gioco all_positions = np.array([[5, 5], [10, 10], [10, 11]]) # Broadcasting: area_positions[:, None, :].shape # (4, 1, 2) all_positions[None, :, :].shape # (1, 3, 2) # Confronto elemento per elemento: matches = (area_positions[:, None, :] == all_positions[None, :, :]).all(axis=2) # matches = [ # [False, False, False], # Esplosione (10,10) vs [(5,5), (10,10), (10,11)] # [False, True, False], # Esplosione (10,11) vs ... # [False, False, True ], # Esplosione (11,10) vs ... # [False, False, False] # Esplosione (11,11) vs ... # ] # Collassa su asse 0 (almeno UNA esplosione colpisce?) unit_hit_mask = matches.any(axis=0) # [False, True, True] # Topo 0 (5,5): NON colpito # Topo 1 (10,10): COLPITO (da esplosione 0) # Topo 2 (10,11): COLPITO (da esplosione 1) ``` ### Calcolo Distanze Vettoriale ```python # Esempio: trovare tutti i topi entro raggio 50 pixel da una bomba bomb_position = np.array([100, 100]) # Posizione bomba # Posizioni di tutti i topi (array NumPy) rat_positions = self.positions[:self.current_size] # Shape: (N, 2) # Calcolo distanze usando broadcasting diff = rat_positions - bomb_position # Shape: (N, 2) # diff[i] = [rat_x - bomb_x, rat_y - bomb_y] distances = np.sqrt((diff ** 2).sum(axis=1)) # Shape: (N,) # distances[i] = sqrt((dx)^2 + (dy)^2) # Trova topi entro raggio within_radius = distances < 50 # Boolean mask hit_rat_indices = np.where(within_radius)[0] # Esempio output: # rat_positions = [[90, 90], [110, 110], [200, 200]] # diff = [[-10, -10], [10, 10], [100, 100]] # distances = [14.14, 14.14, 141.42] # within_radius = [True, True, False] # hit_rat_indices = [0, 1] # Primi due topi colpiti! ``` --- ## 7. Best Practices e Pitfalls ### ✅ Quando Usare NumPy ```python # BUONO: Operazioni su molti dati positions = np.array([[...] for _ in range(1000)]) distances = np.sqrt(((positions - target)**2).sum(axis=1)) # CATTIVO: Operazioni su pochi dati (overhead NumPy!) positions = np.array([[10, 20], [30, 40]]) # Solo 2 elementi distances = np.sqrt(((positions - target)**2).sum(axis=1)) # Più lento di un semplice loop Python! ``` **Regola nel tuo codice:** ```python if len(candidates) < 10: # Usa Python normale for idx in candidates: ... else: # Usa NumPy self._vectorized_collision_check(...) ``` ### ✅ Pre-allocazione vs Append ```python # CATTIVO ❌ (lento con array grandi) array = np.array([]) for i in range(10000): array = np.append(array, i) # O(n) ad ogni append! # BUONO ✅ (veloce) array = np.zeros(10000) for i in range(10000): array[i] = i # O(1) ad ogni assegnazione # MIGLIORE ✅ (senza loop!) array = np.arange(10000) # Operazione vettoriale nativa ``` ### ✅ Memory Layout e Performance ```python # Row-major (C order) - default NumPy array = np.zeros((1000, 3), order='C') # Memoria: [x0, y0, z0, x1, y1, z1, ...] # Veloce per accesso righe: array[i, :] # Column-major (Fortran order) array = np.zeros((1000, 3), order='F') # Memoria: [x0, x1, ..., y0, y1, ..., z0, z1, ...] # Veloce per accesso colonne: array[:, j] # Nel tuo caso (posizioni): self.positions = np.zeros((100, 2)) # Row-major è perfetto # Accesso frequente: self.positions[idx] → [x, y] di un'unità ``` ### ⚠️ Pitfall Comuni #### 1. Copy vs View ```python # View (condivide memoria) a = np.array([1, 2, 3]) b = a[:] # b è una VIEW di a b[0] = 999 print(a) # [999, 2, 3] - modificato anche a! # Copy (memoria separata) a = np.array([1, 2, 3]) b = a.copy() b[0] = 999 print(a) # [1, 2, 3] - a è immutato ``` **Nel tuo codice:** ```python def get_collisions_for_unit(self, ...): # Filtriamo candidati candidate_indices = candidate_indices[mask] # Crea VIEW # Se modifichi candidate_indices dopo, potresti modificare l'originale! # Soluzione: .copy() se necessario ``` #### 2. Broadcasting Inatteso ```python a = np.array([1, 2, 3]) b = np.array([[1], [2], [3]]) # Cosa succede? result = a + b # Broadcasting espande a: # [[1, 2, 3], [[1], [1], [1]] [[2, 3, 4], # [1, 2, 3], + [2], [2], [2]] = [3, 4, 5], # [1, 2, 3]] [3], [3], [3]] [4, 5, 6]] ``` **Verifica sempre le shape:** ```python print(f"Shape: {array.shape}") # Sempre prima di operazioni complesse! ``` #### 3. Integer Overflow ```python # ATTENZIONE con dtype piccoli! a = np.array([250], dtype=np.uint8) # Max 255 b = a + 10 # 260, ma uint8 wrap: diventa 4! # Soluzione: usa dtype appropriati a = np.array([250], dtype=np.int32) # Max ~2 miliardi b = a + 10 # 260 ✅ ``` --- ## Confronto Finale: Prima vs Dopo ### Codice Originale (Il Tuo) ```python # rats.py def update_maze(self): self.unit_positions = {} self.unit_positions_before = {} for unit in self.units.values(): unit.move() self.unit_positions.setdefault(unit.position, []).append(unit) self.unit_positions_before.setdefault(unit.position_before, []).append(unit) for unit in self.units.values(): unit.collisions() # units/rat.py def collisions(self): for other_unit in self.game.unit_positions.get(self.position, []): if other_unit.id == self.id: continue # Controlla collisione... ``` **Complessità**: O(n²) nel caso peggiore **Performance**: ~50ms con 200 unità **Memoria**: Dizionari Python + liste di oggetti ### Codice Ottimizzato (NumPy) ```python # rats.py - 4-pass loop def update_maze(self): self.collision_system.clear() # Pass 1: Pre-registra posizioni for unit in self.units.values(): self.collision_system.register_unit(unit.id, unit.bbox, ...) # Pass 2: Movimento for unit in self.units.values(): unit.move() # Pass 3: Ri-registra dopo movimento self.collision_system.clear() for unit in self.units.values(): self.collision_system.register_unit(unit.id, unit.bbox, ...) # Pass 4: Collisioni for unit in self.units.values(): unit.collisions() # units/rat.py def collisions(self): collisions = self.game.collision_system.get_collisions_for_unit( self.id, self.bbox, self.collision_layer ) for _, other_id in collisions: other_unit = self.game.get_unit_by_id(other_id) if isinstance(other_unit, Rat): # Controlla collisione... ``` **Complessità**: O(n) con spatial hashing + NumPy **Performance**: ~3ms con 250 unità (16x più veloce!) **Memoria**: Array NumPy pre-allocati (più efficiente) --- ## Conclusione ### Cosa Hai Imparato 1. **NumPy**: Array veloci per operazioni matematiche massive 2. **Broadcasting**: Operazioni automatiche su array di dimensioni diverse 3. **Spatial Hashing**: Ridurre O(n²) a O(n) con partizionamento spaziale 4. **Vectorization**: Sostituire loop Python con operazioni NumPy parallele 5. **Pre-allocazione**: Evitare allocazioni ripetute per velocità ### Quando Applicare Queste Tecniche - ✅ **Usa NumPy quando**: Hai 50+ elementi da processare con operazioni matematiche - ✅ **Usa Spatial Hashing quando**: Controlli collisioni/prossimità in spazio 2D/3D - ✅ **Usa Vectorization quando**: Stesso calcolo ripetuto su molti dati - ❌ **Non usare quando**: Pochi elementi (< 10) o logica complessa non matematica ### Risorse per Approfondire - **NumPy Documentation**: https://numpy.org/doc/stable/ - **NumPy Quickstart**: https://numpy.org/doc/stable/user/quickstart.html - **Broadcasting**: https://numpy.org/doc/stable/user/basics.broadcasting.html - **Performance Tips**: https://numpy.org/doc/stable/user/c-info.performance.html --- **Il tuo codice originale era ottimo per il caso d'uso iniziale.** L'ottimizzazione con NumPy è stata necessaria solo quando hai scalato a 200+ unità. Questo è un esempio perfetto di "ottimizza quando serve", non prematuramente! 🎯