46 Commits
Author SHA1 Message Date
Matteo Benedetto e76c6f665a Revert "Redraw wall/bush maze tiles with richer pixel-art detail"
The user asked to go back to the pre-commit assets. The original
BMP_{1..4}_*.png tiles have been restored. The generator script
introduced in that commit is also removed.
2026-06-27 20:21:05 +02:00
Matteo Benedetto 0f14ae3376 Redraw wall/bush maze tiles with richer pixel-art detail
The original BMP_{1..4}_{N,S,E,W,NE,NW,SE,SW,EN,ES,WN,WS}.png tiles were
very noisy low-quality checkerboard patterns. They are now redrawn as
hand-styled pixel-art bush/hedge tiles:

- 5-color per-theme palette (deep shadow, dark, mid, light, highlight)
- irregular wall/passage boundary with natural erosion
- shaded interior (darker in the center = depth)
- scattered leaf clusters and highlights
- kept 32x32 resolution and engine transparent-gray convention

A new generator tool is added at tools/redraw_walls.py. Run it again
with python3 tools/redraw_walls.py to regenerate the tiles if you want
to iterate on the style or palettes.
2026-06-27 20:19:41 +02:00
Matteo Benedetto 5ce084f04d Fix missing state_machine import in controls.py
Resolves NameError when toggling pause (and any other control path
referencing state_machine.GameState). The module was referenced but
never imported after the recent cheat/controls edits.
2026-06-27 20:18:04 +02:00
Matteo Benedetto 4bab8d1a79 Scale gas spread, weapon refill, litter size, and post-mating stop with difficulty
Four gameplay parameters are now driven by the selected difficulty,
following the existing 'hard = harder for the player' philosophy:

  param                       easy   normal   hard
  gas_spread_speed            40     50       90     (higher = slower gas)
  weapon_refill_multiplier    1.3    1.0      0.6    (less ammo on hard)
  max_babies                  2      3        5      (more pups on hard)
  mate_stop_male / female     120/240 100/200 60/120 (shorter stop on hard)

Implementation:
- engine/config.py: new keys on each DIFFICULTY_OPTIONS entry.
- rats.py: _apply_difficulty() loads them onto the game (with __init__
  defaults for safety); normal keeps the previous hardcoded values.
- units/gas.py: Gas.speed reads game.gas_spread_speed (fallback 50).
- engine/unit_manager.py: refill_ammo() scales per-frame refill chances
  by game.weapon_refill_multiplier (fallback 1.0).
- units/rat.py: Male.fuck() uses game.mate_stop_male/mate_stop_female
  and random.randint(1, game.max_babies) (fallbacks to old values).

All reads use getattr with the previous constant as fallback, so older
configs / saved state keep working.
2026-06-27 19:52:27 +02:00
Matteo Benedetto a09b9f0878 Add Ctrl+Return cheat to instantly win the current level
During gameplay, pressing Ctrl+Return triggers an immediate level clear:
the current level is marked as won (level_clear, or run_complete on the
last DAT level) and the state transitions to VICTORY, showing the
normal 'Level Clear!' dialog so the player can then advance with Return.

Implementation:
- engine/sdl2.py: mainloop detects Ctrl+Return via SDLK_RETURN + KMOD_CTRL
  and dispatches a 'cheat_win_level' action (bypassing keybindings).
- engine/controls.py: trigger() now also accepts direct action names that
  are registered in the dispatcher (not key-event names), and a new
  cheat_win_level handler forwards to the game.
- rats.py: MiceMaze.cheat_win_level() performs the win, guarded so it only
  fires while a level is actively being played.

No keybinding files need editing; the cheat is wired through the engine
layer directly.
2026-06-27 19:04:37 +02:00
Matteo Benedetto c09840099e Draw rat normally at tunnel entrances
Rats are now drawn fully (single animation frame) regardless of whether
they are on an open cell, a tunnel entrance, or an internal tunnel
passage. The only case where the rat is not drawn is when its center is
inside a wall (non-empty, non-tunnel cell).

Removed the now-unused _get_tunnel_entrance_direction helper.
2026-06-27 18:56:04 +02:00
Matteo Benedetto 705f3ba260 Remove _draw_partially_hidden_in_tunnel clipping
The clipping logic that sliced the rat sprite when entering/leaving a
single-entrance tunnel produced a jarring half-rat visual. Removed it
entirely: a rat on a single-entrance tunnel cell is now simply not
drawn (it disappears into the tunnel) instead of being partially
clipped. Internal tunnel passages are unchanged and still render the
rat normally.
2026-06-27 18:54:46 +02:00
Matteo Benedetto 1627f58103 Fix rat sprite strip artifact inside internal tunnel cells
After merging new-newnassets, the rat sprite assets are now horizontal
4-frame sprite sheets (e.g. BMP_MALE_UP.png is 80x40 instead of 20x40).
The drawing path for rats inside internal tunnel passages still called
draw_image() without source_rect, so the engine rendered all 4 frames
side by side as a single wide strip (the visual artifact shown in
/tmp/pi-clipboard-...png).

This brings the tunnel-internal rendering in line with the non-tunnel
path, which already used source_rect = (frame * w, 0, w, h).
2026-06-27 18:42:46 +02:00
Matteo Benedetto f348d7e0d9 Merge branch 'merge-newassets-clean' into master
Integrates new LPC-style rat assets and animations from origin/new-newnassets.

Summary:
- 32 new LPC-style sprites (assets/Rat_LPC_Style/)
- 4 animation sprite sheets (assets/Rat_Animated/) for animated rats
- 64 updated Rat sprites in assets/Rat/ with backup originals
- engine/sdl2.py: added 'scale=True' param to load_image()
- engine/graphics.py: rat textures loaded with scale=False
- units/rat.py: animation frame computed from partial_move * 4
- 22 final-version generator scripts in tools/lpc_extract/
- 3 cherry-picked commits from origin/new-newnassets

Excluded from upstream (working artifacts, not appropriate for prod repo):
- ~25 PNG in repo root (male_rats*.png, spritesheet*.png, etc.)
- ~416 tile extracts in tools/lpc_extract/grid/
- scattered debug PNGs (bushes, preview, montages)
- early-iteration scripts (animate_rat v1-v4, restyle v2-v10, etc.)
2026-06-27 17:56:16 +02:00
Matteo Benedetto e26464d843 Merge origin/new-newnassets: new LPC rat assets and animations
Cherry-picked from new-newnassets (3 commits):
- bdf5a8d Add new start animation asset for game launch
- 5f2d09f Fix rat animations and restore map tiles
- 2e2c5bb feat: add LPC-style rat assets, sprite generation scripts, and grid extraction tools

Conflicts resolved:
- engine/sdl2.py: kept 'if scale:' wrapper (compatible, default scale=True)
- units/rat.py: kept animation frame computation (partial_move * 4)
- 12 PNG files (BMP_BABY/MALE/FEMALE_*): kept new-newnassets versions

Cleanup applied: removed ~480 working artifacts not appropriate for the
production repo:
- 25 PNG in repo root (male_rats*.png, spritesheet*.png)
- 416 tile extracts in tools/lpc_extract/grid/
- scattered debug PNGs (bushes, preview, montages, etc.)
- early iteration scripts (animate_rat v1-v4, restyle v2-v10, etc.)

Kept: 32 new LPC-style sprites in assets/Rat_LPC_Style/, 4 animation
sprite sheets in assets/Rat_Animated/, 64 updated Rat sprites, 22
final-version generator scripts, plus all .py/.md updates.
2026-06-27 17:53:45 +02:00
Matteo Benedetto c1662ecf29 Add regression test for gray-strip artifact in asset rendering
Tests/test_gray_strip_artifact.py renders lose.png on a white panel
both with and without the near-white normalization, saves the
results to /tmp/test_loss_before.png and /tmp/test_loss_after.png,
and counts near-white (RGB 240-254, alpha>200) pixels which are the
direct cause of the gray seam.

With the fix the near-white pixel count drops from 3714 to 133
(reduction of 96%), confirming the normalization removes the
artifact.
2026-06-17 11:46:01 +02:00
Matteo Benedetto 2ad6a082b3 Normalize near-white pixels to pure white in all loaded assets
Assets like lose.png contain thousands of (254,254,254) pixels on a
background that is also white. When drawn on a white panel these
near-white pixels are slightly darker, producing a visible gray seam.

Apply a global normalization in load_image(): any opaque pixel with
R=G=B >= 250 is clamped to (255,255,255). Applied to every asset, not
only those with transparent_color, so the background is always pure
white regardless of off-by-one in the source PNG.
2026-06-17 11:13:40 +02:00
Matteo Benedetto 319801d6e5 Fix rat reproduction collision check
Replace the brittle 'self.position == other.position_before' check
with a symmetric cell-intersection test: two rats are considered
colliding if any of their current/previous cells overlap. This handles
both 'both rats in same cell' and 'one rat enters the cell the other
just left'.

Also remove the erroneous hasattr(other_unit, 'fuck') guard that
prevented Male.fuck() from being called on Female (Female has no fuck
method, but only Male should initiate reproduction).

Add tests/test_rat_reproduction.py with 8 scenarios covering overlapping
rats, baby rats, already-pregnant females, far-apart rats, female
self-initiation, sound playback, and procreate interval spawning.
2026-06-17 10:29:26 +02:00
Matteo Benedetto 2eccf504f5 Add semi-transparent tunnel cover overlay for internal passages
- Add sdl2.create_overlay_texture() and draw_overlay_texture(alpha) for
  transparent full-map overlays built from sub-tile surface blits.
- Add Graphics.regenerate_tunnel_cover() which builds an overlay of
  4 random grass sub-tiles (20x20) for every internal tunnel cell,
  defined as a tunnel cell surrounded by occupied cells (wall or
  tunnel) on all four sides. Cells with at least one open side are
  handled by cave_foreground and skipped here.
- Draw the tunnel cover in the game loop after top-layer units/effects
  but before cave_foreground and points, at 95% opacity (alpha=242)
  so the unit and effect passing underneath is just barely visible.
2026-06-16 23:06:17 +02:00
Matteo Benedetto 62a65f599d Draw explosions and gas inside tunnel cells
Remove the is_hidden_in_tunnel() checks from Explosion.draw() and
Gas.draw() so explosions and gas clouds remain visible when their center
falls inside a tunnel cell. Logic already spawned them in tunnel cells,
but they were rendered invisible because the draw path returned early.
2026-06-16 21:56:22 +02:00
Matteo Benedetto 5afdf3705b Render rats inside internal tunnel passages
Previously Rat.draw() hid any rat whose center fell inside a tunnel.
The clipping helper only handled single-entrance cells and returned None
for internal passages/crossroads, causing rats to vanish entirely.

Now internal tunnel cells (those with 0 or 2+ open sides) draw the rat
normally so it remains visible while walking through the tunnel.
Single-entrance cells keep the partial clip effect.
Also fix the visible-ratio math for DOWN/RIGHT clipping: the old
formulas subtracted cell_size from a local coordinate, producing
zero or negative visibility.
2026-06-16 20:37:46 +02:00
Matteo Benedetto b1e9770991 Use wall-only neighbor checks for wall border rendering
Previously regenerate_background() used occupied() (wall or tunnel) to
choose wall border/corner tiles. This caused walls adjacent to tunnels
to render with inner corners as if the tunnel were solid ground.

Now wall border logic uses is_wall() so only actual wall neighbors
influence the shape. Tunnel neighbor checks for flower-suppression are
kept explicit.
2026-06-16 20:24:33 +02:00
Matteo Benedetto 2f8e3e8b28 Fix gas poisoning, explosion hit detection and render order
- Move gas poisoning from Gas.move() to Gas.collisions() so all rats are
  registered in the collision system before the poison query runs.
- Shrink gas and explosion bboxes so rats must be well inside the tile to
  be poisoned/killed.
- Use AABB overlap instead of partial_move threshold for gas poisoning.
- Draw mobile units first, then top-layer effects (gas, mines, bombs,
  explosions) so rats appear under the gas.
- Add draw_on_top hint to Unit base class and top-layer units.
2026-06-16 19:32:50 +02:00
Matteo Benedetto 3e8cd97fda Fix bomb explosions not killing rats and remove tracked pycache
- Move Timer explosion from move() to collisions() so all units are
  registered in the collision system before the kill query runs.
- Explosion units now set a bbox and kill rats that touch them.
- Guard Rat.draw() so dead rats are not drawn.
- Remove units/__pycache__ files from tracking.
2026-06-16 19:15:35 +02:00
John Doe c7ed24483d Add comprehensive test suite for game mechanics and level handling
- Introduced `test_final_level_flow.py` to validate final level transitions and game end scenarios.
- Created `test_game_over_flow.py` to ensure game over conditions trigger correctly based on rat counts.
- Implemented `test_keybindings.py` to verify keybinding configurations and their context-specific actions.
- Developed `test_level_editor.py` to assess level editor functionalities and layout computations.
- Added `test_level_io.py` for testing level data serialization and deserialization.
- Established `test_loop_logic_parity.py` to ensure consistent game state across multiple simulation runs.
- Created `test_non_regression.py` to simulate game behavior and capture states for future verification.
- Implemented `test_verify.py` to compare current game states against a golden master for regression detection.
2026-05-19 22:18:43 +02:00
John Doe 486cd6b7c5 Add non-regression test states and Bluetooth diagnostic script
- Introduced a new JSON file containing non-regression test states with detailed unit information, including positions, ages, and movement directions across multiple frames.
- Added a shell script for Bluetooth diagnostics that checks system information, Bluetooth binaries, running processes, D-Bus status, Bluetooth controller details, and audio stack status, providing a comprehensive overview for troubleshooting.
2026-05-17 23:36:24 +02:00
John Doe dd82ccc087 Add launcher script for microphone visualizer with gamepad support 2026-05-09 17:57:57 +02:00
John Doe 7868d83ced Add microphone visualizer tool using SDL2 for audio input visualization
- Implemented a new Python script `mic_visualizer.py` that captures audio from a microphone and visualizes it in real-time.
- Utilized SDL2 for audio capture and rendering, allowing users to see waveform and spectrum representations of the audio input.
- Added command-line arguments for listing devices, selecting a capture device, and configuring window size and audio settings.
- Included functionality for displaying audio levels and peaks, enhancing user experience with visual feedback.
2026-05-09 17:55:29 +02:00
John Doe d4c73a344b Add Mice! game box art image to MUOS catalogue 2026-05-09 17:27:26 +02:00
John Doe 703e4717e4 Add metadata files for Mice! game in muOS catalogue 2026-05-09 17:22:55 +02:00
John Doe b849e16f69 Add new start animation asset for game launch 2026-05-09 17:08:53 +02:00
John Doe c7ff5ae4cf Refactor code structure for improved readability and maintainability 2026-05-09 16:35:25 +02:00
John Doe ac80210ba5 Add guide button to quit game in start menu keybindings 2026-05-09 13:32:57 +02:00
John Doe d9d7a4ac82 Update keybindings and enhance loading screen functionality
- Refactor keybindings for gas spawning across multiple configurations
- Implement new loading screen updates during game initialization
- Add tests to ensure all weapon actions are exposed in keybinding profiles
- Introduce new assets for explosion effects
2026-05-09 13:29:32 +02:00
John Doe 310dc0dca9 Add DAT finale flow, editor, and new soundtrack assets 2026-05-09 12:03:17 +02:00
John Doe f0d056e7f0 Make muOS launcher prefer local venv 2026-05-08 19:45:25 +02:00
John Doe d32d2cd79c Refine start menu difficulty and preview workflow 2026-05-08 18:26:55 +02:00
John Doe f1770b218c Add animated start menu and level music config 2026-05-07 23:18:39 +02:00
John Doe 2367f4fb1c Add menu audio controls and menu music 2026-05-07 20:35:15 +02:00
John Doe a908b50019 Add looping gameplay music 2026-05-07 20:03:20 +02:00
John Doe 5233294b26 Detect muOS board for keybindings 2026-05-07 19:38:05 +02:00
John Doe 486fea38e7 Implement game controller support and add keybindings for gamepad input 2026-05-07 17:50:07 +02:00
John Doe 9421d8d47c Remove 'kill_rat' keybinding from game controls for cleaner input mapping 2026-05-07 17:49:52 +02:00
John Doe 509b3433b8 Refactor rendering logic in MiceMaze and Rat classes for improved clarity and efficiency 2026-04-14 12:15:16 +02:00
John Doe bbafc3bbba Add new PNG assets for Rat character and environmental elements 2026-04-14 11:35:57 +02:00
John Doe b243cf04d3 Add explosion stage 1 PNG assets for all directions
- Added BMP_1_EXPLOSION_DOWN.png to original and preview directories.
- Added BMP_1_EXPLOSION_LEFT.png to original and preview directories.
- Added BMP_1_EXPLOSION_RIGHT.png to original and preview directories.
- Added BMP_1_EXPLOSION_UP.png to original and preview directories.

These assets are part of the explosion animation for the game, enhancing visual effects during gameplay.
2026-04-14 11:24:16 +02:00
John Doe eaafd92dc2 Enhance asset loading by removing unnecessary surface parameter and adding texture variants for grass and flowers 2026-03-28 16:53:22 +01:00
John Doe b60ffd87aa Refactor code structure for improved readability and maintainability 2026-03-28 16:05:08 +01:00
John Doe 02202e4d3d Add new PNG images for clean and preview outputs
- Added `image_clean.png` to the output directory for the clean image representation.
- Added `image_clean_preview.png` for the preview of the clean image.
- Introduced `image_svg_clean.png` for the SVG clean image representation.
2026-03-27 23:40:27 +01:00
John Doe e7c5ebb119 Add Miniforge environment definition 2026-03-27 22:44:58 +01:00
John Doe 9a86a3734f Add comprehensive NumPy tutorial for optimizing collision detection system 2025-10-24 23:07:57 +02:00
675 changed files with 231065 additions and 2536 deletions
@@ -0,0 +1,107 @@
---
applyTo: "tools/vernon/**,assets/Rat/**"
---
# Pixel Art Sprite Workflow — mice project
## Strumenti disponibili
| Script | Uso |
|--------|-----|
| `tools/vernon/image_to_json.py <INPUT.png> <OUTPUT.json>` | Converte PNG → matrice JSON RGBA 64×64 |
| `tools/vernon/json_to_png.py <INPUT.json> <OUTPUT.png>` | Converte matrice JSON RGBA → PNG |
Entrambi usano Pillow e richiedono il `venv` attivo:
```bash
source .venv/bin/activate
```
## Formato JSON
```json
{
"source": "BMP_BOMB0.png",
"width": 64,
"height": 64,
"mode": "RGBA",
"pixels": [
[ [R, G, B, A], ... ], // riga 0, 64 pixel
... // 64 righe totali
]
}
```
Ogni pixel è `[R, G, B, A]` con valori 0–255.
## Convenzioni cromatiche del gioco
- **Colore trasparente (chromakey):** `[128, 128, 128, 192]` — usato come sfondo, il motore lo rende hidden
- **Alpha standard:** `192` per tutti i pixel visibili (coerente con gli asset originali)
## Workflow iterativo di redesign (passi 0–4)
```
0. BACKUP → prima di sovrascrivere, copia l'originale:
cp assets/Rat/<NAME>.png assets/Rat/backup/<NAME>_original.png
1. image_to_json.py → esamina JSON e PNG originale
2. capire struttura: sfondo, palette, forma principale
3. modificare JSON (o generarlo via script Python) con:
- più livelli di shading (8+ valori invece di 3)
- dettagli geometrici aggiuntivi (texture, bordi, ombre interne)
- palette più ricca mantenendo stile pixel art (bordi netti, no anti-alias)
4. json_to_png.py → valuta risultato visivo; se non soddisfacente, torna a 3
```
## Pattern Python per generare JSON programmaticamente
```python
import json, math
from pathlib import Path
W, H = 64, 64
A = 192 # alpha standard
def px(r, g, b): return [r, g, b, A]
TRANSPARENT = px(128, 128, 128)
grid = [[TRANSPARENT[:] for _ in range(W)] for _ in range(H)]
def put(x, y, col):
if 0 <= x < W and 0 <= y < H:
grid[y][x] = col[:]
# ... disegna su grid ...
data = {"source": "BMP_X.png", "width": W, "height": H, "mode": "RGBA", "pixels": grid}
Path("tools/vernon/output/BMP_X_v2.json").write_text(json.dumps(data, indent=2))
```
## Tecniche pixel art a 64×64
- **Shading sferico:** calcola normale + dot product con luce per N livelli di grigio discreti
- **Rope/miccia:** traccia bezier quadratica, alterna 2–3 toni in sequenza (effetto intrecciato)
- **Scintilla:** pixel centrali chiari (bianco/giallo), bordi che degradano in arancio → rosso
- **Outline:** bordo di 1px nero (`[0,0,0,192]`) attorno a tutte le forme principali
- **Nessun anti-aliasing:** ogni pixel è un colore solido discreto della palette scelta
## Asset da redesignare (tutti 64×64)
| File | Gruppo |
|------|--------|
| `BMP_BOMB0.png` … `BMP_BOMB4.png` | Animazione bomba (0=quieta, 4=accesa) |
| `BMP_1_GRASS_1.png` … `BMP_1_GRASS_4.png` | Tile erba tema 1 (verde) — **redesignate con FBM 7-toni** |
| `BMP_2_GRASS_1.png` … `BMP_2_GRASS_4.png` | Tile erba tema 2 (secca/autunnale) |
| `BMP_3_GRASS_1.png` … `BMP_3_GRASS_4.png` | Tile erba tema 3 (dungeon/pietra) |
| `BMP_4_GRASS_1.png` … `BMP_4_GRASS_4.png` | Tile erba tema 4 (fuoco/lava) |
| `BMP_GAS.png`, `BMP_GAS_{DIR}.png` | Gas generico + 4 direzioni |
| `BMP_EXPLOSION.png`, `BMP_EXPLOSION_{DIR}.png` | Esplosione generica + 4 direzioni |
| `BMP_NUCLEAR.png` | Fungo nucleare |
| `BMP_POISON.png` | Veleno |
## Note sull'animazione BOMB (frame 0–4)
- `BOMB0`: bomba ferma, scintilla piccola a riposo
- `BOMB1`–`BOMB3`: miccia che brucia (la scintilla avanza verso il corpo, la corda si accorcia)
- `BOMB4`: quasi esplode (glow rosso/arancio sul corpo, scintilla grande)
Per i frame animati: mantieni identici corpo + miccia, varia solo posizione/dimensione scintilla e eventuale glow progressivo.
+40
View File
@@ -0,0 +1,40 @@
# Project Guidelines
## UI Preview Tool
When editing the start menu, pause menu, or level intro UI, generate a real preview image before judging layout changes.
Use [tools/render_menu_preview.py](tools/render_menu_preview.py) instead of relying on mental layout or ad-hoc screenshots. The tool renders the actual SDL scene and saves a PNG from the real renderer.
Typical command:
```bash
/home/enne2/dev/mice/.venv/bin/python tools/render_menu_preview.py \
--output /tmp/mice_start_preview.png \
--screen start \
--difficulty normal \
--resolution 1280x720
```
Supported screens:
- `start`
- `pause`
- `level_intro`
Useful flags:
- `--difficulty easy|normal|hard`
- `--resolution WIDTHxHEIGHT`
- `--output /path/to/file.png`
- `--seed N` for deterministic previews
- `--animation-ms N` to choose the GIF frame timestamp for the start menu
Workflow when touching menu layout:
1. Edit the menu code.
2. Run the preview tool for the relevant screen.
3. Inspect the generated PNG.
4. Iterate until spacing and readability are correct.
The preview tool is intended for fast visual feedback and should be preferred before launching a full interactive game session for menu-only changes.
-221
View File
@@ -1,221 +0,0 @@
# Ottimizzazione Sistema di Collisioni con NumPy
## Sommario
Il sistema di collisioni del gioco è stato ottimizzato per gestire **oltre 200 unità simultanee** mantenendo performance elevate (50+ FPS).
## Problema Originale
### Analisi del Vecchio Sistema
1. **Metodo Rat.collisions()**: O(n²) nel caso peggiore
- Ogni ratto controllava tutte le unità nelle sue celle
- Controllo AABB manuale per ogni coppia
- Con molti ratti nella stessa cella, diventava O(n²)
2. **Calcoli bbox ridondanti**
- bbox calcolata in `draw()` ma usata anche in `collisions()`
- Nessun caching
3. **Esplosioni bombe**: Iterazioni multiple sulle stesse posizioni
- Loop annidati per ogni direzione dell'esplosione
- Controllo manuale di `unit_positions` e `unit_positions_before`
4. **Gas**: Controllo vittime a ogni frame anche quando non necessario
## Soluzione Implementata
### Nuovo Sistema: CollisionSystem (engine/collision_system.py)
#### Caratteristiche Principali
1. **Approccio Ibrido**
- < 10 candidati: Metodo semplice senza overhead NumPy
- ≥ 10 candidati: Operazioni vettorizzate con NumPy
- Ottimale per tutti gli scenari
2. **Spatial Hashing**
- Dizionari `spatial_grid` e `spatial_grid_before`
- Lookup O(1) per posizioni
- Solo candidati nella stessa cella vengono controllati
3. **Pre-allocazione Array NumPy**
- Arrays pre-allocati con capacità iniziale di 100
- Raddoppio dinamico quando necessario
- Riduce overhead di `vstack`/`append`
4. **Collision Layers**
- Matrice di collisione 6x6 per filtrare interazioni non necessarie
- Layers: RAT, BOMB, GAS, MINE, POINT, EXPLOSION
- Controllo O(1) se due layer possono collidere
5. **AABB Vettorizzato**
- Controllo collisioni bbox per N unità in una sola operazione
- Broadcasting NumPy per calcoli paralleli
### Struttura del Sistema
```python
class CollisionSystem:
- register_unit() # Registra unità nel frame corrente
- get_collisions_for_unit() # Trova tutte le collisioni per un'unità
- get_units_in_area() # Ottiene unità in più celle (esplosioni)
- check_aabb_collision_vectorized() # AABB vettorizzato
- _simple_collision_check() # Metodo semplice per pochi candidati
```
### Modifiche alle Unità
#### 1. Unit (units/unit.py)
- Aggiunto attributo `collision_layer`
- Inizializzazione con layer specifico
#### 2. Rat (units/rat.py)
- Usa `CollisionSystem.get_collisions_for_unit()`
- Eliminati loop manuali
- Tolleranza AABB gestita dal sistema
#### 3. Bomb (units/bomb.py)
- Esplosioni usano `get_units_in_area()`
- Raccolta posizioni esplosione → query batch
- Singola operazione per trovare tutte le vittime
#### 4. Gas (units/gas.py)
- Usa `get_units_in_cell()` per trovare vittime
- Separazione tra position e position_before
#### 5. Mine (units/mine.py)
- Controllo trigger con `get_units_in_cell()`
- Layer-based detection
### Integrazione nel Game Loop (rats.py)
```python
# Inizializzazione
self.collision_system = CollisionSystem(
self.cell_size, self.map.width, self.map.height
)
# Update loop (3 passaggi)
1. Move: Tutte le unità si muovono
2. Register: Registrazione nel collision system + backward compatibility
3. Collisions + Draw: Controllo collisioni e rendering
```
## Performance
### Test Results (250 unità su griglia 30x30)
**Stress Test - 100 frames:**
```
Total time: 332.41ms
Average per frame: 3.32ms
FPS capacity: 300.8 FPS
Target (50 FPS): ✓ PASS
```
### Confronto Scenari Reali
| Numero Unità | Frame Time | FPS Capacity |
|--------------|------------|--------------|
| 50 | ~0.5ms | 2000 FPS |
| 100 | ~1.3ms | 769 FPS |
| 200 | ~2.5ms | 400 FPS |
| 250 | ~3.3ms | 300 FPS |
| 300 | ~4.0ms | 250 FPS |
**Conclusione**: Il sistema mantiene **performance eccellenti** anche con 300+ unità, ben oltre il target di 50 FPS.
### Vantaggi per Scenari Specifici
1. **Molti ratti in poche celle**:
- Vecchio: O(n²) per celle dense
- Nuovo: O(n) con spatial hashing
2. **Esplosioni bombe**:
- Vecchio: Loop annidati per ogni direzione
- Nuovo: Singola query batch per tutte le posizioni
3. **Scalabilità**:
- Vecchio: Degrada linearmente con numero unità
- Nuovo: Performance costante grazie a spatial hashing
## Compatibilità
- **Backward compatible**: Mantiene `unit_positions` e `unit_positions_before`
- **Rimozione futura**: Questi dizionari possono essere rimossi dopo test estesi
- **Nessuna breaking change**: API delle unità invariata
## File Modificati
1. ✅ `requirements.txt` - Aggiunto numpy
2. ✅ `engine/collision_system.py` - Nuovo sistema (370 righe)
3. ✅ `units/unit.py` - Aggiunto collision_layer
4. ✅ `units/rat.py` - Ottimizzato collisions()
5. ✅ `units/bomb.py` - Esplosioni vettorizzate
6. ✅ `units/gas.py` - Query ottimizzate
7. ✅ `units/mine.py` - Detection ottimizzata
8. ✅ `units/points.py` - Aggiunto collision_layer
9. ✅ `rats.py` - Integrato CollisionSystem nel game loop
10. ✅ `test_collision_performance.py` - Benchmark suite
## Prossimi Passi (Opzionali)
1. **Rimozione backward compatibility**: Eliminare `unit_positions`/`unit_positions_before`
2. **Profiling avanzato**: Identificare ulteriori bottleneck
3. **Spatial grid gerarchico**: Per mappe molto grandi (>100x100)
4. **Caching bbox**: Se le unità non si muovono ogni frame
## Installazione
```bash
cd /home/enne2/Sviluppo/mice
source .venv/bin/activate
pip install numpy
```
## Testing
```bash
# Benchmark completo
python test_collision_performance.py
# Gioco normale
./mice.sh
```
## Note Tecniche
### Approccio Ibrido Spiegato
Il sistema usa un **threshold di 10 candidati** per decidere quando usare NumPy:
- **< 10 candidati**: Loop Python semplice (no overhead numpy)
- **≥ 10 candidati**: Operazioni vettorizzate NumPy
Questo è ottimale perché:
- Con pochi candidati, l'overhead di creare array NumPy supera i benefici
- Con molti candidati, la vettorizzazione compensa l'overhead iniziale
### Memory Layout
```
Arrays NumPy (pre-allocati):
- bboxes: (capacity, 4) float32 → ~1.6KB per 100 unità
- positions: (capacity, 2) int32 → ~800B per 100 unità
- layers: (capacity,) int8 → ~100B per 100 unità
Total: ~2.5KB per 100 unità (trascurabile)
```
## Conclusioni
L'ottimizzazione con NumPy è **altamente efficace** per il caso d'uso di Mice! con 200+ unità:
✅ Performance eccellenti (300+ FPS con 250 unità)
✅ Scalabilità lineare grazie a spatial hashing
✅ Backward compatible
✅ Approccio ibrido ottimale per tutti gli scenari
✅ Memory footprint minimo
Il sistema è **pronto per la produzione**.
+45
View File
@@ -8,12 +8,24 @@ Mice! is a strategic game where players must kill rats with bombs before they re
## Features
- **Maze Generation**: Randomly generated mazes using Depth First Search (DFS) algorithm.
- **Original Level Support**: Loads the original `level.dat` from `assets/Rat/level.dat` when present and falls back to `maze.json` otherwise.
- **Units**: Different types of units such as rats, bombs, and points with specific behaviors.
- **Graphics**: Custom graphics for maze tiles, units, and effects.
- **Sound Effects**: Audio feedback for various game events.
- **Scoring**: Points system to track player progress.
- **Performance**: Optimized collision detection system supporting 200+ simultaneous units using NumPy vectorization.
## Utilities
### Microphone Visualizer
A small SDL2 microphone visualizer is available in `tools/mic_visualizer.py`.
- List capture devices: `python tools/mic_visualizer.py --list-devices`
- Open the default microphone: `python tools/mic_visualizer.py`
- Open a specific input: `python tools/mic_visualizer.py --device-index 1`
- On muOS, use `mice_mic.sh` as a launcher in `ROMS/Ports` and it will run fullscreen with gamepad quit support.
## Engine Architecture
The Mice! game engine is built on a modular architecture designed for flexibility and maintainability. The engine follows a component-based design pattern where different systems handle specific aspects of the game.
@@ -72,6 +84,7 @@ The Mice! game engine is built on a modular architecture designed for flexibilit
- **Map Class**: Manages the game world structure
- **Features**:
- Maze data loading and parsing
- DAT archive parsing for the original 32 built-in RATS levels
- Collision detection system
- Tile-based world representation
- Pathfinding support for AI units
@@ -238,6 +251,38 @@ Units interact through a centralized collision and event system:
- **Libraries**:
- `numpy` 2.3.4 for vectorized collision detection
- `sdl2` for graphics and window management
## Map Editor
The project now includes a Tkinter editor for `level.dat` archives:
- Launch with `python tools/level_editor.py`
- Open a specific archive with `python tools/level_editor.py --file assets/Rat/level.dat`
- Start on a specific level with `python tools/level_editor.py --file assets/Rat/level.dat --level 7`
- The editor requires a Python installation with the standard `tkinter` module available at OS level
Editor capabilities:
- Edits the full 32-level DAT archive used by the game
- Creates new DAT archives with 32 default levels
- Paints `EMPTY`, `WALL`, and `TUNNEL` tiles with brush, fill, and rectangle tools
- Supports undo/redo, level copy/paste, and level duplication between slots
- Imports a single level from JSON and exports the current level back to JSON
- Validates common gameplay issues such as missing spawn cells, open borders, and disconnected traversable areas
## Level Sources
- Preferred source: `assets/Rat/level.dat`
- Fallback source: `maze.json`
- Current behavior: the loader can read any level from the DAT archive via `--level N`, while still falling back to `maze.json` when the DAT is unavailable.
- Tile semantics are now preserved internally from the original format: `0=EMPTY`, `1=WALL`, `2=TUNNEL`.
- Rendering uses those semantics directly: walls use themed grass/flower tiles, tunnel cells use themed cave tiles, and empty cells remain the generic walkable tunnel floor used by the Python version.
### Run examples
- `python rats.py`
- `python rats.py --level 7`
- `python rats.py --map maze.json`
- `Pillow` for image processing
- `uuid` for unique unit identification
- `subprocess` for playing sound effects
-308
View File
@@ -1,308 +0,0 @@
# Game Profile Manager
A PySDL2-based user profile management system designed for gamepad-only control with virtual keyboard input. This system allows players to create, edit, delete, and select user profiles for games using only gamepad inputs or directional keys, with no need for physical keyboard text input.
## Features
- **640x480 Resolution**: Optimized for retro gaming systems and handheld devices
- **Create New Profiles**: Add new user profiles with custom names using virtual keyboard
- **Profile Selection**: Browse and select active profiles
- **Edit Settings**: Modify profile settings including difficulty, volume levels, and preferences
- **Delete Profiles**: Remove unwanted profiles
- **Gamepad/Directional Navigation**: Full control using only gamepad/joystick inputs or arrow keys
- **Virtual Keyboard**: Text input using directional controls - no physical keyboard typing required
- **JSON Storage**: Profiles stored in human-readable JSON format
- **Persistent Settings**: All changes automatically saved
## Installation
### Requirements
- Python 3.6+
- PySDL2
- SDL2 library
### Setup
```bash
# Install required Python packages
pip install pysdl2
# For Ubuntu/Debian users, you may also need:
sudo apt-get install libsdl2-dev libsdl2-ttf-dev
# Make launcher executable
chmod +x launch_profile_manager.sh
```
## Usage
### Running the Profile Manager
```bash
# Method 1: Use the launcher script
./launch_profile_manager.sh
# Method 2: Run directly with Python
python3 profile_manager.py
```
### Gamepad Controls
#### Standard Gamepad Layout (Xbox/PlayStation compatible)
- **D-Pad/Hat**: Navigate menus up/down/left/right, control virtual keyboard cursor
- **Button 0 (A/X)**: Confirm selection, enter menus, select virtual keyboard characters
- **Button 1 (B/Circle)**: Go back, cancel action
- **Button 2 (X/Square)**: Delete profile, backspace in virtual keyboard
- **Button 3 (Y/Triangle)**: Reserved for future features
#### Keyboard Controls (Alternative)
- **Arrow Keys**: Navigate menus and virtual keyboard cursor
- **Enter/Space**: Confirm selection, select virtual keyboard characters
- **Escape**: Go back, cancel action
- **Delete/Backspace**: Delete profile, backspace in virtual keyboard
- **Tab**: Reserved for future features
#### Virtual Keyboard Text Input
When creating or editing profile names:
1. **Navigate**: Use D-Pad/Arrow Keys to move cursor over virtual keyboard
2. **Select Character**: Press A/Enter to add character to profile name
3. **Backspace**: Press X/Delete to remove last character
4. **Complete**: Navigate to "DONE" and press A/Enter to finish input
5. **Cancel**: Navigate to "CANCEL" and press A/Enter to abort
#### Navigation Flow
1. **Main Menu**: Create Profile → Select Profile → Edit Settings → Exit
2. **Profile List**: Choose from existing profiles, or go back
3. **Create Profile**: Use virtual keyboard to enter name, confirm with directional controls
4. **Edit Profile**: Adjust settings using left/right navigation
### Display Specifications
- **Resolution**: 640x480 pixels (4:3 aspect ratio)
- **Optimized for**: Retro gaming systems, handheld devices, embedded systems
- **Font Scaling**: Adaptive font sizes for optimal readability at low resolution
### Profile Structure
Profiles are stored in `user_profiles.json` with the following structure:
```json
{
"profiles": {
"PlayerName": {
"name": "PlayerName",
"created_date": "2024-01-15T10:30:00",
"last_played": "2024-01-20T14:45:00",
"games_played": 25,
"total_score": 15420,
"best_score": 980,
"settings": {
"difficulty": "normal",
"sound_volume": 75,
"music_volume": 60,
"screen_shake": true,
"auto_save": true
},
"achievements": [
"first_win",
"score_500"
]
}
},
"active_profile": "PlayerName"
}
```
## Integration with Games
### Loading Active Profile
```python
import json
def load_active_profile():
try:
with open('user_profiles.json', 'r') as f:
data = json.load(f)
active_name = data.get('active_profile')
if active_name and active_name in data['profiles']:
return data['profiles'][active_name]
except (FileNotFoundError, json.JSONDecodeError):
pass
return None
# Usage in your game
profile = load_active_profile()
if profile:
difficulty = profile['settings']['difficulty']
sound_volume = profile['settings']['sound_volume']
```
### Updating Profile Stats
```python
def update_profile_stats(score, game_completed=True):
try:
with open('user_profiles.json', 'r') as f:
data = json.load(f)
active_name = data.get('active_profile')
if active_name and active_name in data['profiles']:
profile = data['profiles'][active_name]
if game_completed:
profile['games_played'] += 1
profile['total_score'] += score
profile['best_score'] = max(profile['best_score'], score)
profile['last_played'] = datetime.now().isoformat()
with open('user_profiles.json', 'w') as f:
json.dump(data, f, indent=2)
except Exception as e:
print(f"Error updating profile: {e}")
```
## Customization
### Adding New Settings
Edit the `UserProfile` dataclass and the settings adjustment methods:
```python
# In profile_manager.py, modify the UserProfile.__post_init__ method
def __post_init__(self):
if self.settings is None:
self.settings = {
"difficulty": "normal",
"sound_volume": 50,
"music_volume": 50,
"screen_shake": True,
"auto_save": True,
"your_new_setting": "default_value" # Add here
}
```
### Custom Font
Place your font file in the `assets/` directory and update the font path:
```python
font_path = "assets/your_font.ttf"
```
### Screen Resolution
The application is optimized for 640x480 resolution. To change resolution, modify the window size in the init_sdl method:
```python
self.window = sdl2.ext.Window(
title="Profile Manager",
size=(your_width, your_height) # Change from (640, 480)
)
```
### Virtual Keyboard Layout
Customize the virtual keyboard characters by modifying the keyboard_chars list:
```python
self.keyboard_chars = [
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'],
['K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T'],
['U', 'V', 'W', 'X', 'Y', 'Z', '1', '2', '3', '4'],
['5', '6', '7', '8', '9', '0', '_', '-', ' ', '<'],
['DONE', 'CANCEL', '', '', '', '', '', '', '', '']
]
```
## Troubleshooting
### No Gamepad Detected
- Ensure your gamepad is connected before starting the application
- Try different USB ports
- Check if your gamepad is recognized by your system
- The application will show "No gamepad detected - using keyboard fallback"
- Virtual keyboard works with both gamepad and keyboard controls
### Font Issues
- Ensure the font file exists in the assets directory
- The system will fall back to default font if custom font is not found
- Supported font formats: TTF, OTF
- Font sizes are automatically scaled for 640x480 resolution
### Virtual Keyboard Not Responding
- Ensure you're in text input mode (creating/editing profile names)
- Use arrow keys or D-Pad to navigate the virtual keyboard cursor
- Press Enter/A button to select characters
- The virtual keyboard cursor should be visible as a highlighted character
### Profile Not Saving
- Check file permissions in the application directory
- Ensure sufficient disk space
- Verify JSON format is not corrupted
### Resolution Issues
- The application is designed for 640x480 resolution
- On higher resolution displays, the window may appear small
- This is intentional for compatibility with retro gaming systems
- Content is optimized and readable at this resolution
## File Structure
```
project_directory/
├── profile_manager.py # Main application (640x480, virtual keyboard)
├── launch_profile_manager.sh # Launcher script
├── user_profiles.json # Profile data storage
├── test_profile_manager.py # Test suite for core functions
├── game_profile_integration.py # Example game integration
├── assets/
│ └── decterm.ttf # Font file (optional)
└── README_PROFILE_MANAGER.md # This documentation
```
## Development Notes
### Virtual Keyboard Implementation
The virtual keyboard is implemented as a 2D grid of characters:
- Cursor position tracked with (keyboard_cursor_x, keyboard_cursor_y)
- Character selection adds to input_text string
- Special functions: DONE (confirm), CANCEL (abort), < (backspace)
- Fully navigable with directional controls only
### Screen Layout for 640x480
- Header area: 0-80px (titles, status)
- Content area: 80-400px (main UI elements)
- Controls area: 400-480px (help text, instructions)
- All elements scaled and positioned for optimal readability
### Adding New Screens
1. Add screen name to `current_screen` handling
2. Create render method (e.g., `render_new_screen()`)
3. Add navigation logic in input handlers
4. Update screen transitions in confirm/back handlers
### Gamepad Button Mapping
The application uses SDL2's joystick interface. Button numbers may vary by controller:
- Most modern controllers follow the Xbox layout
- PlayStation controllers map similarly but may have different button numbers
- Test with your specific controller and adjust mappings if needed
### Performance Considerations
- Rendering is capped at 60 FPS for smooth operation
- Input debouncing prevents accidental rapid inputs
- JSON operations are minimized and occur only when necessary
- Virtual keyboard rendering optimized for 640x480 resolution
- Font scaling automatically adjusted for readability
### Adding Support for Different Resolutions
To support different screen resolutions, modify these key areas:
1. Window initialization in `init_sdl()`
2. Panel and button positioning in render methods
3. Font size scaling factors
4. Virtual keyboard grid positioning
### Gamepad Integration Notes
- Uses SDL2's joystick interface for maximum compatibility
- Button mapping follows standard Xbox controller layout
- Hat/D-Pad input prioritized over analog sticks for precision
- Input timing designed for responsive but not accidental activation
## Target Platforms
This profile manager is specifically designed for:
- **Handheld Gaming Devices**: Steam Deck, ROG Ally, etc.
- **Retro Gaming Systems**: RetroPie, Batocera, etc.
- **Embedded Gaming Systems**: Custom arcade cabinets, portable devices
- **Low-Resolution Displays**: 640x480, 800x600, and similar resolutions
- **Gamepad-Only Environments**: Systems without keyboard access
## License
This profile manager is provided as-is for educational and personal use. Designed for integration with retro and handheld gaming systems.
-466
View File
@@ -1,466 +0,0 @@
# Analisi Performance Rendering SDL2 - Mice!
## Sommario Esecutivo
Il sistema di rendering presenta **diverse criticità** che possono causare cali di FPS con molte unità (200+). Ho identificato 7 problemi principali e relative soluzioni.
---
## 🔴 CRITICITÀ IDENTIFICATE
### 1. **Controllo Visibilità Inefficiente** ⚠️ ALTA PRIORITÀ
**Problema:**
```python
def is_in_visible_area(self, x, y):
return (-self.w_offset - self.cell_size <= x <= self.width - self.w_offset and
-self.h_offset - self.cell_size <= y <= self.height - self.h_offset)
```
Ogni `draw_image()` chiama `is_in_visible_area()` che fa **4 confronti** per ogni sprite.
**Impatto con 250 unità:**
- 250 unità × 4 confronti = **1000 operazioni per frame**
- Molte unità potrebbero essere fuori schermo ma vengono controllate comunque
**Soluzione:**
```python
# Opzione A: Culling a livello di game loop (CONSIGLIATA)
# Filtra unità PRIMA del draw usando spatial grid
visible_cells = get_visible_cells(w_offset, h_offset, viewport_width, viewport_height)
for unit in units:
if unit.position in visible_cells or unit.position_before in visible_cells:
unit.draw()
# Opzione B: Cache dei bounds
class GameWindow:
def update_viewport_bounds(self):
self.visible_x_min = -self.w_offset - self.cell_size
self.visible_x_max = self.width - self.w_offset
self.visible_y_min = -self.h_offset - self.cell_size
self.visible_y_max = self.height - self.h_offset
def is_in_visible_area(self, x, y):
return (self.visible_x_min <= x <= self.visible_x_max and
self.visible_y_min <= y <= self.visible_y_max)
```
**Guadagno stimato:** 10-15% con 200+ unità
---
### 2. **Chiamate renderer.copy() Non Batch** ⚠️ ALTA PRIORITÀ
**Problema:**
```python
# Ogni unità chiama renderer.copy() individualmente
def draw_image(self, x, y, sprite, tag=None, anchor="nw"):
if not self.is_in_visible_area(x, y):
return
sprite.position = (x + self.w_offset, y + self.w_offset)
self.renderer.copy(sprite, dstrect=sprite.position) # ← Singola chiamata SDL
```
**Impatto:**
- 250 unità = **250 chiamate individuali a SDL2**
- Ogni chiamata ha overhead di context switch
- Non sfrutta batching hardware
**Soluzione - Sprite Batching:**
```python
class GameWindow:
def __init__(self, ...):
self.sprite_batch = [] # Accumula sprite da disegnare
def queue_sprite(self, x, y, sprite):
"""Accoda sprite invece di disegnarlo subito"""
if self.is_in_visible_area(x, y):
self.sprite_batch.append((sprite, x + self.w_offset, y + self.h_offset))
def flush_sprites(self):
"""Disegna tutti gli sprite in batch"""
for sprite, x, y in self.sprite_batch:
sprite.position = (x, y)
self.renderer.copy(sprite, dstrect=sprite.position)
self.sprite_batch.clear()
# Nel game loop
for unit in units:
unit.draw() # Ora usa queue_sprite invece di draw_image
renderer.flush_sprites() # Singolo flush alla fine
```
**Guadagno stimato:** 15-25% con 200+ unità
---
### 3. **Calcolo Posizioni Ridondante** ⚠️ MEDIA PRIORITÀ
**Problema in Rat.draw():**
```python
def draw(self):
start_perf = self.game.render_engine.get_perf_counter() # ← Non utilizzato!
direction = self.calculate_rat_direction() # ← Già calcolato in move()
# Calcolo partial_x/y ripetuto per ogni frame
if direction in ["UP", "DOWN"]:
partial_y = self.partial_move * self.game.cell_size * (1 if direction == "DOWN" else -1)
else:
partial_x = self.partial_move * self.game.cell_size * (1 if direction == "RIGHT" else -1)
x_pos = self.position_before[0] * self.game.cell_size + ...
y_pos = self.position_before[1] * self.game.cell_size + ...
# get_image_size() chiamato ogni frame
image_size = self.game.render_engine.get_image_size(image)
```
**Impatto:**
- `calculate_rat_direction()`: già calcolato in `move()` → **250 chiamate duplicate**
- `get_image_size()`: dimensioni statiche, non cambiano → **250 lookups inutili**
- Calcoli aritmetici ripetuti
**Soluzione - Cache in Unit:**
```python
class Rat(Unit):
def move(self):
# ... existing move logic ...
self.direction = self.calculate_rat_direction() # Cache direction
# Pre-calcola render_position durante move
self._update_render_position()
def _update_render_position(self):
"""Pre-calcola posizione di rendering"""
if self.direction in ["UP", "DOWN"]:
partial_y = self.partial_move * self.game.cell_size * (1 if self.direction == "DOWN" else -1)
partial_x = 0
else:
partial_x = self.partial_move * self.game.cell_size * (1 if self.direction == "RIGHT" else -1)
partial_y = 0
image_size = self.game.rat_image_sizes[self.sex if self.age > AGE_THRESHOLD else "BABY"][self.direction]
self.render_x = self.position_before[0] * self.game.cell_size + (self.game.cell_size - image_size[0]) // 2 + partial_x
self.render_y = self.position_before[1] * self.game.cell_size + (self.game.cell_size - image_size[1]) // 2 + partial_y
self.bbox = (self.render_x, self.render_y, self.render_x + image_size[0], self.render_y + image_size[1])
def draw(self):
sex = self.sex if self.age > AGE_THRESHOLD else "BABY"
image = self.game.rat_assets_textures[sex][self.direction]
self.game.render_engine.draw_image(self.render_x, self.render_y, image, tag="unit")
```
**Pre-cache dimensioni immagini in Graphics:**
```python
class Graphics:
def load_assets(self):
# ... existing code ...
# Pre-cache image sizes
self.rat_image_sizes = {}
for sex in ["MALE", "FEMALE", "BABY"]:
self.rat_image_sizes[sex] = {}
for direction in ["UP", "DOWN", "LEFT", "RIGHT"]:
texture = self.rat_assets_textures[sex][direction]
self.rat_image_sizes[sex][direction] = texture.size
```
**Guadagno stimato:** 5-10% con 200+ unità
---
### 4. **Tag System Inutilizzato** ⚠️ BASSA PRIORITÀ
**Problema:**
```python
def delete_tag(self, tag):
"""Placeholder for tag deletion (not implemented)"""
pass
# Ogni draw passa tag="unit" ma non viene mai usato
unit.draw() # → draw_image(..., tag="unit")
```
**Impatto:**
- Overhead minimo di passaggio parametro inutile
- 250 unità × parametro = spreco memoria call stack
**Soluzione:**
Rimuovere parametro `tag` da `draw_image()` e tutte le chiamate.
**Guadagno stimato:** 1-2%
---
### 5. **Generazione Blood Stains Costosa** ⚠️ MEDIA PRIORITÀ
**Problema:**
```python
def add_blood_stain(self, position):
# Genera nuova surface SDL con pixel manipulation
new_blood_surface = self.render_engine.generate_blood_surface() # LENTO
if position in self.blood_stains:
# Combina surfaces con pixel blending
combined_surface = self.render_engine.combine_blood_surfaces(...) # MOLTO LENTO
# WORST: Rigenera TUTTO il background
self.background_texture = None # ← Forza rigenerazione completa
```
**Impatto:**
- Ogni morte di ratto → rigenerazione background completo
- 200 morti = **200 rigenerazioni** di texture enorme
- `generate_blood_surface()`: loop pixel-by-pixel
- `combine_blood_surfaces()`: blending manuale RGBA
**Soluzione - Pre-generazione + Overlay Layer:**
```python
class Graphics:
def load_assets(self):
# Pre-genera 10 varianti di blood stains
self.blood_stain_pool = [
self.render_engine.generate_blood_surface()
for _ in range(10)
]
self.blood_stain_textures = [
self.render_engine.factory.from_surface(surface)
for surface in self.blood_stain_pool
]
# Layer separato per blood
self.blood_layer_sprites = []
def add_blood_stain(self, position):
"""Aggiunge blood come sprite invece che rigenerare background"""
import random
blood_texture = random.choice(self.blood_stain_textures)
x = position[0] * self.cell_size
y = position[1] * self.cell_size
self.blood_layer_sprites.append((blood_texture, x, y))
def draw_blood_layer(self):
"""Disegna tutti i blood stains come sprites"""
for texture, x, y in self.blood_layer_sprites:
self.render_engine.draw_image(x, y, texture, tag="blood")
# Nel game loop
self.draw_maze() # Background statico (UNA SOLA VOLTA)
self.draw_blood_layer() # Blood stains come sprites
# ... draw units ...
```
**Guadagno stimato:** 20-30% durante scenari con molte morti
---
### 6. **Font Manager Creazione Inefficiente** ⚠️ BASSA PRIORITÀ
**Problema:**
```python
def generate_fonts(self, font_file):
fonts = {}
for i in range(10, 70, 1): # 60 font managers!
fonts.update({i: sdl2.ext.FontManager(font_path=font_file, size=i)})
return fonts
```
**Impatto:**
- 60 FontManager creati all'avvio
- Usa solo 3-4 dimensioni durante il gioco
- Memoria sprecata: ~60 × FontManager overhead
**Soluzione - Lazy Loading:**
```python
def generate_fonts(self, font_file):
self.font_file = font_file
self.fonts = {}
# Pre-carica solo dimensioni comuni
common_sizes = [20, 35, 45]
for size in common_sizes:
self.fonts[size] = sdl2.ext.FontManager(font_path=font_file, size=size)
def get_font(self, size):
"""Lazy load font se non esiste"""
if size not in self.fonts:
self.fonts[size] = sdl2.ext.FontManager(font_path=self.font_file, size=size)
return self.fonts[size]
```
**Guadagno:** Startup time: -200ms, Memoria: -5MB
---
### 7. **Performance Counter Inutilizzato** ⚠️ MINIMA PRIORITÀ
**Problema in Rat.draw():**
```python
def draw(self):
start_perf = self.game.render_engine.get_perf_counter() # Mai usato!
# ... resto del codice ...
```
**Impatto:**
- 250 chiamate a `SDL_GetPerformanceCounter()` per niente
- Overhead chiamata: ~0.001ms × 250 = 0.25ms/frame
**Soluzione:**
Rimuovere la riga o usarla per profiling reale.
---
## 📊 IMPATTO TOTALE STIMATO
### Performance Attuali (Stimate)
Con 250 unità:
- Collision detection: ~3.3ms (✅ ottimizzato)
- Rendering: **~10-15ms** (🔴 collo di bottiglia)
- Game logic: ~2ms
- **TOTALE: ~15-20ms/frame** (50-65 FPS)
### Performance Post-Ottimizzazione
Con 250 unità:
- Collision detection: ~3.3ms
- Rendering: **~4-6ms** (✅ migliorato 2.5x)
- Game logic: ~2ms
- **TOTALE: ~9-11ms/frame** (90-110 FPS)
---
## 🎯 PIANO DI IMPLEMENTAZIONE CONSIGLIATO
### Priority 1 - Quick Wins (1-2 ore)
1. ✅ **Viewport culling** (soluzione A - spatial grid)
2. ✅ **Cache render positions** in Rat
3. ✅ **Pre-cache image sizes**
4. ✅ **Rimuovi tag parameter**
**Guadagno atteso: 20-30%**
### Priority 2 - Medium Effort (2-3 ore)
5. ✅ **Blood stain overlay layer** (invece di rigenerazione)
6. ✅ **Sprite batching** (queue + flush)
**Guadagno atteso: +30-40% cumulativo = 50-70% totale**
### Priority 3 - Optional (1 ora)
7. ✅ **Lazy font loading**
8. ✅ **Rimuovi performance counter inutilizzato**
**Guadagno atteso: marginale ma cleanup code**
---
## 🔧 OTTIMIZZAZIONI AVANZATE (Opzionali)
### A. Texture Atlas per Rat Sprites
**Problema:** 250 ratti = 250 texture bind per frame
**Soluzione:**
```python
# Combina tutti i rat sprites in una singola texture
# Usa source rectangles per selezionare sprite specifici
rat_atlas = create_texture_atlas(all_rat_sprites)
renderer.copy(rat_atlas, srcrect=sprite_rect, dstrect=screen_rect)
```
**Guadagno:** +10-20% con 200+ unità
### B. Dirty Rectangle Tracking
**Problema:** Ridisegna tutto il background ogni frame
**Soluzione:**
```python
# Traccia solo le aree che sono cambiate
dirty_rects = []
for unit in units:
if unit.moved:
dirty_rects.append(unit.previous_rect)
dirty_rects.append(unit.current_rect)
# Ridisegna solo dirty rects
for rect in dirty_rects:
redraw_region(rect)
```
**Guadagno:** +30-50% su mappe grandi
### C. Multi-threaded Rendering
**Problema:** Single-threaded rendering
**Soluzione:**
```python
# Thread 1: Game logic + collision
# Thread 2: Preparazione sprite (calcolo posizioni, culling)
# Main thread: Solo rendering SDL
```
**Guadagno:** +40-60% su CPU multi-core
---
## 📈 METRICHE DI SUCCESSO
Dopo le ottimizzazioni Priority 1 e 2:
| Unità | FPS Attuale | FPS Target | FPS Atteso |
|-------|-------------|------------|------------|
| 50 | ~60 | 60 | 60+ |
| 100 | ~55 | 60 | 60+ |
| 200 | ~45 | 50 | 70-80 |
| 250 | ~35-40 | 50 | 60-70 |
| 300 | ~30 | 50 | 50-60 |
---
## 🧪 STRUMENTI DI PROFILING
### Script di Benchmark Rendering
```python
# test_rendering_performance.py
import time
from rats import MiceMaze
def benchmark_rendering():
game = MiceMaze('maze.json')
# Spawna 250 ratti
for _ in range(250):
game.spawn_rat()
# Misura 100 frame
render_times = []
for _ in range(100):
start = time.perf_counter()
# Solo rendering (no game logic)
game.draw_maze()
for unit in game.units.values():
unit.draw()
game.renderer.present()
render_times.append((time.perf_counter() - start) * 1000)
print(f"Avg render time: {sum(render_times)/len(render_times):.2f}ms")
print(f"Min: {min(render_times):.2f}ms, Max: {max(render_times):.2f}ms")
```
---
## 💡 CONCLUSIONI
Il rendering è **il principale bottleneck** con 200+ unità, non le collisioni.
**Ottimizzazioni critiche:**
1. Viewport culling (15% gain)
2. Sprite batching (25% gain)
3. Blood stain overlay (30% gain in scenari con morti)
4. Cache render positions (10% gain)
**Implementando Priority 1 + 2 si ottiene ~2.5x speedup sul rendering**, portando il gioco da ~40 FPS a ~70-80 FPS con 250 unità.
Il sistema di collisioni NumPy è già ottimizzato (3.3ms), quindi il focus deve essere sul rendering SDL2.
-259
View File
@@ -1,259 +0,0 @@
# Ottimizzazioni Rendering Implementate
## ✅ Completato - 24 Ottobre 2025
### Modifiche Implementate
#### 1. **Cache Viewport Bounds** ✅ (+15% performance)
**File:** `engine/sdl2.py`
**Problema:** `is_in_visible_area()` ricalcolava i bounds ogni chiamata (4 confronti × 250 unità = 1000 operazioni/frame)
**Soluzione:**
```python
def _update_viewport_bounds(self):
"""Update cached viewport bounds for fast visibility checks"""
self.visible_x_min = -self.w_offset - self.cell_size
self.visible_x_max = self.width - self.w_offset
self.visible_y_min = -self.h_offset - self.cell_size
self.visible_y_max = self.height - self.h_offset
def is_in_visible_area(self, x, y):
"""Ottimizzato con cached bounds"""
return (self.visible_x_min <= x <= self.visible_x_max and
self.visible_y_min <= y <= self.visible_y_max)
```
I bounds vengono aggiornati solo quando cambia il viewport (scroll), non a ogni check.
---
#### 2. **Pre-cache Image Sizes** ✅ (+5% performance)
**File:** `engine/graphics.py`
**Problema:** `get_image_size()` chiamato 250 volte/frame anche se le dimensioni sono statiche
**Soluzione:**
```python
# All'avvio, memorizza tutte le dimensioni
self.rat_image_sizes = {}
for sex in ["MALE", "FEMALE", "BABY"]:
self.rat_image_sizes[sex] = {}
for direction in ["UP", "DOWN", "LEFT", "RIGHT"]:
texture = self.rat_assets_textures[sex][direction]
self.rat_image_sizes[sex][direction] = texture.size # Cache!
```
Le dimensioni vengono lette una sola volta all'avvio, non ogni frame.
---
#### 3. **Cache Render Positions in Rat** ✅ (+10% performance)
**File:** `units/rat.py`
**Problema:**
- `calculate_rat_direction()` chiamato sia in `move()` che in `draw()` → duplicato
- Calcoli aritmetici (partial_x, partial_y, x_pos, y_pos) ripetuti ogni frame
- `get_image_size()` chiamato ogni frame (ora risolto con cache)
**Soluzione:**
```python
def move(self):
# ... movimento ...
self.direction = self.calculate_rat_direction()
self._update_render_position() # Pre-calcola per draw()
def _update_render_position(self):
"""Pre-calcola posizione di rendering durante move()"""
sex = self.sex if self.age > AGE_THRESHOLD else "BABY"
image_size = self.game.rat_image_sizes[sex][self.direction] # Cache!
# Calcola una sola volta
if self.direction in ["UP", "DOWN"]:
partial_x = 0
partial_y = self.partial_move * self.game.cell_size * (1 if self.direction == "DOWN" else -1)
else:
partial_x = self.partial_move * self.game.cell_size * (1 if self.direction == "RIGHT" else -1)
partial_y = 0
self.render_x = self.position_before[0] * self.game.cell_size + (self.game.cell_size - image_size[0]) // 2 + partial_x
self.render_y = self.position_before[1] * self.game.cell_size + (self.game.cell_size - image_size[1]) // 2 + partial_y
self.bbox = (self.render_x, self.render_y, self.render_x + image_size[0], self.render_y + image_size[1])
def draw(self):
"""Semplicissimo - usa solo valori pre-calcolati"""
sex = self.sex if self.age > AGE_THRESHOLD else "BABY"
image = self.game.rat_assets_textures[sex][self.direction]
self.game.render_engine.draw_image(self.render_x, self.render_y, image, tag="unit")
```
**Benefici:**
- Nessun calcolo duplicato
- `draw()` diventa semplicissimo
- `bbox` aggiornato automaticamente per collision system
---
#### 4. **Blood Stains come Overlay Layer** ✅ (+30% in scenari con morti)
**File:** `engine/graphics.py`
**Problema:**
- Ogni morte di ratto → `generate_blood_surface()` (pixel-by-pixel loop)
- Poi → `combine_blood_surfaces()` (blending RGBA manuale)
- Infine → `self.background_texture = None` → **rigenerazione completa background**
- Con 200 morti = 200 rigenerazioni di texture enorme!
**Soluzione:**
**A) Pre-generazione pool all'avvio:**
```python
def load_assets(self):
# ...
# Pre-genera 10 varianti di blood stains
self.blood_stain_textures = []
for _ in range(10):
blood_surface = self.render_engine.generate_blood_surface()
blood_texture = self.render_engine.draw_blood_surface(blood_surface, (0, 0))
if blood_texture:
self.blood_stain_textures.append(blood_texture)
self.blood_layer_sprites = [] # Lista di blood sprites
```
**B) Blood come sprites overlay:**
```python
def add_blood_stain(self, position):
"""Aggiunge blood come sprite - NESSUNA rigenerazione background!"""
import random
blood_texture = random.choice(self.blood_stain_textures)
x = position[0] * self.cell_size
y = position[1] * self.cell_size
# Aggiungi alla lista invece di rigenerare
self.blood_layer_sprites.append((blood_texture, x, y))
def draw_blood_layer(self):
"""Disegna tutti i blood stains come sprites"""
for blood_texture, x, y in self.blood_layer_sprites:
self.render_engine.draw_image(x, y, blood_texture, tag="blood")
```
**C) Background statico:**
```python
def draw_maze(self):
if self.background_texture is None:
self.regenerate_background()
self.render_engine.draw_background(self.background_texture)
self.draw_blood_layer() # Blood come overlay separato
```
**Benefici:**
- Background generato UNA SOLA VOLTA (all'inizio)
- Blood stains: pre-generati → nessun costo runtime
- Nessuna rigenerazione costosa
- 10 varianti casuali per varietà visiva
---
### Performance Stimate
#### Prima delle Ottimizzazioni
Con 250 unità:
```
Frame breakdown:
- Collision detection: 3.3ms (già ottimizzato con NumPy)
- Rendering: 10-15ms
- draw_image checks: ~2ms (visibility checks)
- get_image_size calls: ~1ms
- Render calculations: ~2ms
- Blood regenerations: ~3-5ms (picchi)
- SDL copy calls: ~4ms
- Game logic: 2ms
TOTALE: ~15-20ms → 50-65 FPS
```
#### Dopo le Ottimizzazioni
Con 250 unità:
```
Frame breakdown:
- Collision detection: 3.3ms (invariato)
- Rendering: 5-7ms ✅
- draw_image checks: ~0.5ms (cached bounds)
- get_image_size calls: 0ms (pre-cached)
- Render calculations: ~0.5ms (pre-calcolati in move)
- Blood regenerations: 0ms (overlay sprites)
- SDL copy calls: ~4ms (invariato)
- Game logic: 2ms
TOTALE: ~10-12ms → 80-100 FPS
```
**Miglioramento: ~2x più veloce nel rendering**
---
### Metriche di Successo
| Unità | FPS Prima | FPS Dopo | Miglioramento |
|-------|-----------|----------|---------------|
| 50 | ~60 | 60+ | Stabile |
| 100 | ~55 | 60+ | +9% |
| 200 | ~45 | 75-85 | +67-89% |
| 250 | ~35-40 | 60-70 | +71-100% |
| 300 | ~30 | 55-65 | +83-117% |
---
### File Modificati
1. ✅ `engine/sdl2.py` - Cache viewport bounds
2. ✅ `engine/graphics.py` - Pre-cache sizes + blood overlay
3. ✅ `units/rat.py` - Cache render positions
**Linee di codice modificate:** ~120 linee
**Tempo implementazione:** ~2 ore
**Performance gain:** 2x rendering, 1.5-2x FPS totale con 200+ unità
---
### Ottimizzazioni Future (Opzionali)
#### Non Implementate (basso impatto):
- ❌ Rimozione tag parameter (1-2% gain)
- ❌ Sprite batching (complesso, 15-25% gain ma richiede refactor)
- ❌ Texture atlas (10-20% gain ma richiede asset rebuild)
#### Motivo:
Le ottimizzazioni implementate hanno già raggiunto l'obiettivo di 60+ FPS con 250 unità. Le ulteriori ottimizzazioni avrebbero costo/beneficio sfavorevole.
---
### Testing
**Come testare i miglioramenti:**
1. Avvia il gioco: `./mice.sh`
2. Spawna molti ratti (usa keybinding per spawn)
3. Osserva FPS counter in alto a sinistra
4. Usa bombe per uccidere ratti → osserva che NON ci sono lag durante morti multiple
**Risultati attesi:**
- Con 200+ ratti: FPS stabile 70-85
- Durante esplosioni multiple: nessun lag
- Blood stains appaiono istantaneamente
---
### Conclusioni
✅ **Obiettivo raggiunto**: Da ~40 FPS a ~70-80 FPS con 250 unità
Le ottimizzazioni si concentrano sui bottleneck reali:
1. **Viewport checks** erano costosi → ora cached
2. **Image sizes** venivano riletti → ora cached
3. **Render calculations** erano duplicati → ora pre-calcolati
4. **Blood stains** rigeneravano tutto → ora overlay
Il sistema ora scala bene fino a 300+ unità mantenendo 50+ FPS.
Il rendering SDL2 è ora **2x più veloce** e combinato con il collision system NumPy già ottimizzato, il gioco può gestire scenari con centinaia di unità senza problemi di performance.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 B

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 B

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 B

After

Width:  |  Height:  |  Size: 296 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 B

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 419 B

After

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 425 B

After

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 428 B

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 B

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 B

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 443 B

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 B

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 B

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 B

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 B

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 449 B

After

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 392 B

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 392 B

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 392 B

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 391 B

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 B

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 B

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 B

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 B

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

After

Width:  |  Height:  |  Size: 191 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 B

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 B

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 B

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 B

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 B

After

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 B

After

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 377 B

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 B

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 409 B

After

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 407 B

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 402 B

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 B

After

Width:  |  Height:  |  Size: 281 B

Some files were not shown because too many files have changed in this diff Show More